Compare commits

..

70 commits

Author SHA1 Message Date
erik
5fe0f85369 Test for alex 2025-06-22 23:44:04 +02:00
erik
bc68d29ba5 Added loader 2025-06-22 22:35:50 +02:00
erik
8b3c800b3f v5.0.0.0 2025-06-22 22:31:21 +02:00
erik
be7e8302cd Correct version 2025-06-22 22:26:58 +02:00
erik
6120966c05 Added hashed based checking of updates. 2025-06-22 22:22:10 +02:00
erik
e9925096f0 Debug 2025-06-22 21:32:00 +02:00
erik
c174c143c6 Fixed bug in quests 2025-06-22 17:53:24 +02:00
erik
553a2388d1 Fixed quest bug when hotrealoading 2025-06-22 17:13:38 +02:00
erik
e9a113abdd Added /mm sendinventory 2025-06-22 16:56:44 +02:00
erik
ab425a04cc test7 2025-06-22 13:22:25 +02:00
erik
6b631c3fe8 test6 2025-06-22 13:21:34 +02:00
erik
130615c141 test5 2025-06-22 13:20:18 +02:00
erik
4ab0992979 test4 2025-06-22 13:17:22 +02:00
erik
741d17af0c test3 2025-06-22 13:15:44 +02:00
erik
52633e2a1a test2 2025-06-22 13:14:32 +02:00
erik
7cb917ce67 Test 2025-06-22 13:13:39 +02:00
erik
af98555052 New reload file 2025-06-22 13:10:51 +02:00
erik
7e80fff4b6 Added hotreload 2025-06-22 13:03:14 +02:00
erik
73ba7082d8 Added hot reload 2025-06-22 12:10:15 +02:00
erik
bb493febb4 added z coords for portal discovery and minster disovery 2025-06-21 10:48:53 +02:00
erik
91cd934878 version 4.0.0.2 2025-06-17 20:59:38 +02:00
erik
31c9042ed3 version 4.0.0.2 fixed flapping players in Overlord, changed how taper is counted, added safe websocket marshalling 2025-06-17 20:58:10 +02:00
erik
f9644baf1e Added taper counts, vitals streaming and tentacle porn 2025-06-10 22:40:42 +02:00
erik
ecea5af243 Version 3.0.1.5 2025-06-09 02:17:43 +02:00
erik
1142a012ef final fix 2025-06-09 02:15:27 +02:00
erik
57b2f0400e te 2025-06-09 02:03:11 +02:00
erik
01151e679b test2 2025-06-09 01:48:20 +02:00
erik
2e6ac9553f test 2025-06-09 01:34:45 +02:00
erik
a3ce9ce2df fix(websocket): stop rare “ReceiveAsync already outstanding” crashes
• Guard Start() so only one connection loop can run per client.
• Capture the socket inside each receive loop, so reconnects can’t
  leave old loops fighting over the same ClientWebSocket.
2025-06-09 00:26:05 +02:00
erik
c61912607a fixed bug with stacksize 2025-06-08 23:39:12 +02:00
erik
8cf9a59061 fixed a bug in stacksize same version 2025-06-08 23:38:22 +02:00
erik
fda5c0417e v 3.0.1.3 2025-06-08 22:47:07 +02:00
erik
01c762d669 Version 3.0.1.3 2025-06-08 22:46:27 +02:00
erik
28bdf7f312 Added inventory over websockets, death reporting, taper reporting. 2025-06-08 22:45:22 +02:00
erik
ebf6fd0bf7 added new xml views 2025-06-08 00:25:50 +02:00
erik
e9b5378ba6 v 3.0.1.2 2025-06-08 00:21:34 +02:00
erik
23e33599ca Added Mossy tracker, know issue 2025-06-08 00:15:55 +02:00
erik
7eb98491d3 Fix 2025-06-01 14:05:29 +02:00
erik
c7a684eacd Version 3.0.1.1 2025-06-01 09:54:56 +02:00
erik
b662e360a2 version 3.0.1.1 2025-06-01 09:54:13 +02:00
erik
96b85ed226 Now with Plugin chat intercept. version 3.0.1.1 2025-05-31 22:18:41 +02:00
erik
591da42d36 added vtank interface so we can advance navpoints 2025-05-30 08:35:40 +02:00
erik
8c43ed676c deleted old view 2025-05-29 21:34:52 +02:00
erik
afabfdef0e 3.0.1.0 2025-05-29 21:34:35 +02:00
erik
79304baaad New GUI overhaul!, version 3.0.1.0 2025-05-29 21:34:16 +02:00
erik
c05d6c9d1b added nav 3.0.0.6 2025-05-29 17:58:56 +02:00
erik
1ddfc9fbdf 3.0.0.6 2025-05-29 17:56:44 +02:00
erik
1f85d9c6f0 New visual routes and new GUI 2025-05-29 17:52:30 +02:00
erik
037e5cd940 assembly for 3.0.0.5 2025-05-28 20:44:53 +02:00
erik
f3da44901f Version 3.0.0.5 2025-05-28 20:39:33 +02:00
erik
c3d158aabb added updater 2025-05-28 00:15:12 +02:00
erik
985b69fe01 Changed to another GUID 2025-05-27 23:10:10 +02:00
erik
e68f2c9801 Merge branch 'spawn-detection' of git.snakedesert.se:SawatoMosswartsEnjoyersClub/MosswartMassacre into spawn-detection 2025-05-27 21:48:56 +02:00
erik
a070075c1f version 3.0.0.3 2025-05-27 21:48:39 +02:00
erik
19442301bc version 3.0.0.3 2025-05-27 21:47:48 +02:00
erik
052fc1b71e new release 2025-05-27 21:30:16 +02:00
erik
a91556c949 Fixed webcommand bug 2025-05-27 21:29:43 +02:00
erik
6fcfe5fc21 3.0.0.1 2025-05-24 21:40:10 +02:00
erik
a0f40cf2cd Client telemetry 2025-05-24 21:10:45 +02:00
erik
f4ec57a44d Fixed inventory, spawn and settings 2025-05-15 21:34:20 +02:00
erik
0c539bc023 removed unvated 2025-05-12 20:28:46 +02:00
erik
1e8e134593 removed GearCycler 2025-05-12 20:28:17 +02:00
erik
de2057789a Inventory logger 2025-05-12 20:27:00 +02:00
erik
29fba4b7cb test of inventory 2025-05-11 15:48:48 +02:00
erik
781a7767ee version 3.0.0.0 2025-05-09 23:14:25 +02:00
erik
ff3cb69f98 version 3.0..0.0 2025-05-09 23:14:08 +02:00
erik
9a6fa191a0 New release! Spawndetection and websockets 2025-05-09 23:08:39 +02:00
erik
33fb228654 Cleaned up websockets 2025-05-09 23:07:24 +02:00
erik
56b09f509a Now with spaws detection 2025-05-08 20:21:40 +02:00
erik
d2e9988bdd Websockets-version 2025-05-05 20:08:15 +02:00
355 changed files with 45238 additions and 7693 deletions

View file

@ -0,0 +1,22 @@
{
"permissions": {
"allow": [
"Bash(mkdir:*)",
"Bash(grep:*)",
"Bash(rg:*)",
"Bash(sed:*)",
"Bash(find:*)",
"Bash(true)",
"Bash(rm:*)",
"Bash(cp:*)",
"Bash(chmod:*)",
"Bash(./cleanup.sh:*)",
"Bash(bash:*)",
"Bash(ls:*)",
"Bash(python3:*)",
"Bash(msbuild:*)",
"Bash(dotnet build:*)"
]
},
"enableAllProjectMcpServers": false
}

7
.gitignore vendored
View file

@ -360,4 +360,9 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd
/UI_Creation_Manual.md
/UI_Creation_VirindiViewService_Manual.md
/MosswartMassacre/Unused
/MosswartMassacre/Decal.Adapter.csproj
/MosswartMassacre/Decal.Interop.Core.csproj

78
CLAUDE.md Normal file
View file

@ -0,0 +1,78 @@
- when we porting flagtracker there mus be no simplification or trunkation of the code
- you can use utility belt for references about how to check views and create them perhaps also for quests. Same with the folder Mag-Tools
# Icon Implementation Discoveries
## DECAL Icon System - CRITICAL DIFFERENCES
### Spell Icons vs Item Icons
**SPELL ICONS**: Use RAW icon values from DECAL FileService SpellTable
- Access via: `fileService.SpellTable.GetById(spellId)` then reflection to find icon property
- **NO OFFSET REQUIRED** - Use raw icon value directly
- Matches original Lua: `game.Character.SpellBook.Get(spellID).Icon` (no offset)
**ITEM ICONS**: Require MagTools-style offset
- Format: `itemIcon + 0x6000000`
- Used for inventory items, weapons, armor, etc.
### Working Implementation
```csharp
// For SPELL icons - NO offset
private int GetRealSpellIcon(int spellId)
{
var fileService = CoreManager.Current.Filter<Decal.Filters.FileService>();
var spell = fileService.SpellTable.GetById(spellId);
// Use reflection to find icon property
return iconValue; // RAW value, no offset
}
// For ITEM icons - WITH offset
int itemIconId = worldObject.Icon + 0x6000000;
```
### VVS Icon Display
- Use `DecalControls.IconColumn` in XML (NOT PictureColumn)
- IconColumn creates HudPictureBox controls automatically
- Set icon via: `((HudPictureBox)control).Image = iconId;`
### Successful Applications
**Flag Tracker Recalls Tab**: Real spell icons working perfectly
**MagTools Reference**: Item icons with 0x6000000 offset
**Original Lua Compatibility**: Exact replication of flagtracker behavior
### Implemented Icon Applications
**Flag Tracker Recalls Tab**: Real spell icons working perfectly
**Flag Tracker Cantrips Tab**: Multi-layered icon system implemented
- **Attributes**: Spell icons (Strength, Endurance, etc.) with background support
- **Protection Auras**: Spell icons (Armor, Flame Ward, etc.)
- **Skills**: Dynamic skill icons from character training data
### Cantrips Tab Icon System (Advanced Implementation)
Based on original Lua flagtracker's sophisticated 3-icon approach:
```csharp
// Attributes: Background + Spell Icon overlay
["Strength"] = new CantripInfo {
SpellIconId = 1354, // StrengthSelf8 spell icon
BackgroundIconId = 0x060013F9 // UI background icon
};
// Protection Auras: Spell icons only
["Armor"] = new CantripInfo {
SpellIconId = 1397 // ArmorSelf8 spell icon
};
// Skills: Dynamic skill icons from character data
IconId = GetSkillIconId(skillId) // Real skill icon + 0x6000000 offset
```
### Icon Priority System
1. **Background + Spell Overlay**: For attributes (complex layering)
2. **Spell Icons Only**: For protection auras
3. **Skill Icons**: From character skill data (item-style offset)
4. **Fallback**: Default portal icon (0x6002D14)
### Future Icon Usage
- **Weapons Tab**: 3-layer system (underlay + icon + overlay) from original Lua
- **Luminance Auras**: Could use custom UI icons
- **Society Items**: Could use item icons with proper offset

View file

@ -1,45 +0,0 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using Decal.Adapter;
using Decal.Adapter.Wrappers;
using VirindiViewService;
using VirindiViewService.Controls;
namespace GearCycler
{
[ComVisible(true)]
[Guid("9b6a07e1-ae78-47f4-b09c-174f6a27d7a3")] // Replace with your own unique GUID if needed
[FriendlyName("GearCycler")]
public class GearCore : PluginBase
{
public HudView view;
private HudButton btnCycle;
protected override void Startup()
{
try
{
string xml = File.ReadAllText("ViewXML\\mainview.xml");
view = HudView.ReadXmlLayout(xml);
view.Visible = true;
btnCycle = (HudButton)view.Controls["btnCycle"];
btnCycle.Hit += (s, e) =>
{
CoreManager.Current.Actions.AddChatText("[GearCycler] Button clicked!", 1);
};
}
catch (Exception ex)
{
CoreManager.Current.Actions.AddChatText($"[GearCycler] Failed to load UI: {ex.Message}", 1);
}
}
protected override void Shutdown()
{
btnCycle?.Dispose();
view?.Dispose();
}
}
}

View file

@ -1,81 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{1293560E-2A56-417F-8116-8CE0420DC97C}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>GearCycler</RootNamespace>
<AssemblyName>GearCycler</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>TRACE;DEBUG;VVS_REFERENCED;DECAL_INTEROP</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Decal.Adapter">
<HintPath>..\MosswartMassacre\lib\Decal.Adapter.dll</HintPath>
</Reference>
<Reference Include="Decal.Interop.Core, Version=2.9.8.3, Culture=neutral, PublicKeyToken=481f17d392f1fb65, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<EmbedInteropTypes>True</EmbedInteropTypes>
<HintPath>..\MosswartMassacre\lib\Decal.Interop.Core.DLL</HintPath>
</Reference>
<Reference Include="Decal.Interop.Inject, Version=2.9.8.3, Culture=neutral, PublicKeyToken=481f17d392f1fb65, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<EmbedInteropTypes>True</EmbedInteropTypes>
<HintPath>..\MosswartMassacre\lib\Decal.Interop.Inject.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="VirindiViewService">
<HintPath>..\MosswartMassacre\lib\VirindiViewService.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="GearCore.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Include="ViewXML\mainView.xml" />
</ItemGroup>
<ItemGroup>
<Content Include="mainView.xml" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View file

@ -1,33 +0,0 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("GearCycler")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("GearCycler")]
[assembly: AssemblyCopyright("Copyright © 2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("f5462318-d26a-4ab0-8981-80edd9ec9c99")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View file

@ -1,63 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace GearCycler.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GearCycler.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}

View file

@ -1,101 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View file

@ -1,6 +0,0 @@
<?xml version="1.0"?>
<view icon="112" title="GearCycler" width="300" height="200">
<control progid="DecalControls.FixedLayout" clipped="">
<control progid="DecalControls.PushButton" name="btnCycle" left="10" top="10" width="150" height="30" text="Cycle Gear"/>
</control>
</view>

Binary file not shown.

View file

@ -1,386 +0,0 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>VirindiViewService</name>
</assembly>
<members>
<member name="F:VirindiViewService.WriteTextFormats.None">
<summary>
Implies Top and Left
</summary>
</member>
<member name="T:VirindiViewService.HudViewDrawStyle">
<summary>
Provides theme elements, which can be drawn by controls.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudThemeElement">
<summary>
Displays an element from the current theme.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudControl">
<summary>
The base class for all Virindi Views controls.
</summary>
</member>
<member name="M:VirindiViewService.Controls.HudControl.Initialize">
<summary>
Called after this control is added to a ControlGroup. This is when the Name and details have been set.
</summary>
</member>
<member name="M:VirindiViewService.Controls.HudControl.AddChild(VirindiViewService.Controls.HudControl)">
<summary>
Add and initialize a child control of this control. The child may be removed by disposing it.
</summary>
<param name="ctrl"></param>
</member>
<member name="M:VirindiViewService.Controls.HudControl.RemovedChild(VirindiViewService.Controls.HudControl)">
<summary>
Called when a child of this control is disposed.
</summary>
<param name="ch"></param>
</member>
<member name="M:VirindiViewService.Controls.HudControl.Dispose">
<summary>
Recursively disposes all children and removes this control from the view, if it is initialized.
</summary>
</member>
<member name="M:VirindiViewService.Controls.HudControl.MouseWheel(System.Drawing.Point,System.Int32)">
<summary>
Handles a mouse wheel event. Parent controls must pass this on to applicable children if necessary.
</summary>
<param name="pt"></param>
<param name="amt"></param>
</member>
<member name="M:VirindiViewService.Controls.HudControl.MouseDown(System.Drawing.Point)">
<summary>
Fires the MouseEvent event for mouse down, and sets this control as the focus control if CanTakeFocus is true.
Parent controls must pass this on to applicable children if necessary.
</summary>
<param name="pt"></param>
</member>
<member name="M:VirindiViewService.Controls.HudControl.MouseUp(System.Drawing.Point,System.Drawing.Point)">
<summary>
Fires the MouseEvent event for mouse up as well as the Hit event.
Parent controls must pass this on to applicable children if necessary.
</summary>
<param name="pt"></param>
<param name="orig"></param>
</member>
<member name="M:VirindiViewService.Controls.HudControl.ExternalMouseUp(System.Drawing.Point)">
<summary>
Fired when the mousedown originated outside the current view. The base version of this method
passes on the event to all children if the 'up' point is within its saved rect.
</summary>
<param name="pt">Mouseup point</param>
</member>
<member name="M:VirindiViewService.Controls.HudControl.MouseMove(System.Drawing.Point)">
<summary>
Tracks mouseover and fires the MouseOverChange event, as well as the MouseEvent event for mouse move.
Parent controls must pass this on to applicable children if necessary.
</summary>
<param name="pt"></param>
</member>
<member name="M:VirindiViewService.Controls.HudControl.RawKeyAction(System.Int16,System.Int32,System.Int32,System.Boolean@)">
<summary>
Parses a key message and fires the specific key event methods.
Key events are only sent to the control with focus.
</summary>
<param name="Msg"></param>
<param name="WParam"></param>
<param name="LParam"></param>
<param name="Eat"></param>
</member>
<member name="M:VirindiViewService.Controls.HudControl.DrawNow(VirindiViewService.DxTexture)">
<summary>
WARNING: ONLY A PARENT CONTROL SHOULD CALL THIS METHOD.
This method is overridden in derived controls to handle the actual control drawing. Overridden methods should call
the base, draw, and recursively call this method on all child controls.
</summary>
<param name="iSavedTarget"></param>
</member>
<member name="M:VirindiViewService.Controls.HudControl.SetClipRegion(System.Drawing.Rectangle,VirindiViewService.HudViewDrawStyle,VirindiViewService.DrawOptions,System.Drawing.Rectangle)">
<summary>
WARNING: ONLY A PARENT CONTROL SHOULD CALL THIS METHOD.
Notifies a control of changed saved draw options. This method saves its parameters in the Savedxxx properties.
Parent controls should override this method and recursively notify children of their new draw options, altering
their pClipRegion to reflect their new position in the View.
This base method also fires the DrawStateChange and ThemeChanged events.
</summary>
<param name="pClipRegion">This control's area, relative to the view area.</param>
<param name="pStyle">The theme applied to this control.</param>
<param name="pContext">The context of this control, eg. inside a listbox.</param>
<param name="pViewRect">The position of the View, in game window coordinates.</param>
</member>
<member name="P:VirindiViewService.Controls.HudControl.CanDraw">
<summary>
WARNING: ONLY A PARENT CONTROL SHOULD SET THIS PROPERTY.
</summary>
</member>
<member name="P:VirindiViewService.Controls.HudControl.XMLAttributes">
<summary>
List of XmlAttributes present on the XmlNode that was used to construct this control, if the control was loaded from XML. Otherwise, empty.
</summary>
</member>
<member name="P:VirindiViewService.Controls.HudControl.XMLNode">
<summary>
The XmlNode used to construct this control, if the control was loaded from XML. Otherwise, null.
</summary>
</member>
<member name="P:VirindiViewService.Controls.HudControl.InternalName">
<summary>
The name that this control will be initialized with.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudConsole">
<summary>
A multiline uneditable scrolling text box.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudPictureBox">
<summary>
A single image control.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudImageButton">
<summary>
A button using custom images.
</summary>
</member>
<member name="T:MyClasses.HashedList`1">
<summary>
A doubly-linked list with a Dictionary index. Duplicate items are not allowed.
-Add is O(1)
-Contains is O(1)
-Remove is O(1)
-Get/set by index is O(n)
-Insert is O(n)
-RemoveAt is O(n)
Additionally, a cached pointer (with associated index) is kept pointing to the last used index item.
When looking up an item by index, the list is walked from the head, tail, or cached index pointer.
Thus, doing multiple operations in index order is O(1) even without an enumerator.
</summary>
<typeparam name="T"></typeparam>
</member>
<member name="M:MyClasses.HashedList`1.RunToIndex(System.Int32)">
<summary>
This method gets the node corresponding to a particular index. To get there,
the list is traversed from the head, tail, or cached index pointer (if valid).
</summary>
<param name="ind"></param>
<returns></returns>
</member>
<member name="T:VirindiViewService.Controls.HudBrowser">
<summary>
Web browser control, using Awesomium (free license version)
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudHScrollBar">
<summary>
A horizontal scrollbar.
</summary>
</member>
<member name="T:Ciper.AC.AC_Text">
<summary>
Summary description for ByteCursor.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudCheckBox">
<summary>
A checkbox with optional associated text. Uses its parent to provide the background.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudTextBox">
<summary>
A single-line text input box.
</summary>
</member>
<member name="M:VirindiViewService.IElementRenderer.Measure(VirindiViewService.HudViewDrawStyle,System.Drawing.Size)">
<summary>
Called before render so the required size of the new target area can be calculated.
The returned value is the size of the desired draw area, not including outer borders and
style-dependent padding. This size must be less than or equal to MaximumSize in each dimension.
</summary>
<param name="style"></param>
<returns></returns>
</member>
<member name="M:VirindiViewService.IElementRenderer.Render(VirindiViewService.IRenderTarget,System.Drawing.Rectangle,VirindiViewService.HudViewDrawStyle)">
<summary>
Draw this element. When this is called, the background and borders will already have been drawn, and
target will already be in BeginRender. This method should leave the target in render mode.
</summary>
<param name="target"></param>
<param name="drawregion"></param>
<param name="style"></param>
</member>
<member name="T:VirindiViewService.cTipStringRenderer">
<summary>
A renderer for string-only tooltips.
</summary>
</member>
<member name="T:MyClasses.HashedSet`1">
<summary>
Represents an unordered set of items. Duplicates are not allowed.
(This is really just a dictionary which only holds keys.)
Should be used when a collection of non-duplicate items is needed and
the order doesn't matter.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudTabView">
<summary>
A series of titled tabs along the top, each one having an associated control which appears
on the bottom when its tab is enabled.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudProgressBar">
<summary>
A progressbar.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudButton">
<summary>
A regular pushbutton-style control.
</summary>
</member>
<member name="M:VirindiViewService.Service.Game_D3DBeginSceneOriginal">
<summary>
Calls the non-hooked IDirect3DDevice9::BeginScene function. When rendering inside a VVS view or texture, use DxTexture.BeginRender() instead.
</summary>
<returns></returns>
</member>
<member name="M:VirindiViewService.Service.Game_D3DEndSceneOriginal">
<summary>
Calls the non-hooked IDirect3DDevice9::EndScene function. When rendering inside a VVS view or texture, use DxTexture.EndRender() instead.
</summary>
<returns></returns>
</member>
<member name="P:VirindiViewService.Service.HudBarInstance">
<summary>
Gets the current instance of the VVS bar.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudChatbox">
<summary>
A console containing game chat.
</summary>
</member>
<member name="M:VirindiViewService.DxTexture.BeginRender(System.Boolean,System.Boolean,System.Int32,System.Int32,System.Int32)">
<summary>
Initializes Direct3D drawing and sets the rendertarget to this texture. Calls to this method should be minimized to improve performance. DxTexture.EndRender() must be called after calling this method.
</summary>
<param name="AlphaTestEnable"></param>
<param name="SeparateAlphaEnable"></param>
<param name="SourceBlendAlpha"></param>
<param name="DestinationBlendAlpha"></param>
<param name="BlendOperation"></param>
</member>
<member name="M:VirindiViewService.DxTexture.EndRender">
<summary>
Ends Direct3D rendering and resets the rendertarget. Must be called after DxTexture.BeginRender().
</summary>
</member>
<member name="M:VirindiViewService.DxTexture.DrawLine(System.Drawing.PointF,System.Drawing.PointF,System.Drawing.Color,System.Single)">
<summary>
Note: Before use, FlushSprite() may need to be called to ensure correct ordering.
</summary>
<param name="p1"></param>
<param name="p2"></param>
<param name="color"></param>
<param name="width"></param>
</member>
<member name="M:VirindiViewService.DxTexture.DXDrawUserPrimitives(Microsoft.DirectX.Direct3D.PrimitiveType,System.Int32,System.Object,Microsoft.DirectX.Direct3D.VertexFormats)">
<summary>
Note: Before use, you must call BeginUserDrawOperation().
</summary>
<param name="ptype"></param>
<param name="count"></param>
<param name="data"></param>
<param name="vertexformat"></param>
</member>
<member name="M:VirindiViewService.DxTexture.DXDrawUserPrimitives(Microsoft.DirectX.Direct3D.PrimitiveType,System.Int32,System.Object,Microsoft.DirectX.Direct3D.VertexFormats,VirindiViewService.DxTexture)">
<summary>
Note: Before use, you must call BeginUserDrawOperation().
</summary>
<param name="ptype"></param>
<param name="count"></param>
<param name="data"></param>
<param name="vertexformat"></param>
<param name="texture"></param>
</member>
<member name="T:VirindiViewService.Controls.HudList">
<summary>
A vertically scrolling list, containing a number of rows and columns. Every row
has the same number and types of columns. Each column contains a specified control type.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudImageStack">
<summary>
A number of images on top of each other, which always draw in the proper order.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudStaticText">
<summary>
A simple text display control. Uses its parent to provide the background.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudFixedLayout">
<summary>
A container for multiple controls with set locations and sizes within.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudCombo">
<summary>
A dropdown list.
</summary>
</member>
<member name="M:VirindiViewService.ContextMenu`1.Show(System.Drawing.Point)">
<summary>
If the context menu is not visible, it is created at the specified point.
</summary>
</member>
<member name="M:VirindiViewService.ContextMenu`1.Show(System.Drawing.Point,VirindiViewService.HudViewDrawStyle)">
<summary>
If the context menu is not visible, it is created at the specified point with the specified theme.
</summary>
</member>
<member name="T:VirindiViewService.TooltipSystem.cTooltipInfo">
<summary>
Provides information about an associated tooltip.
</summary>
</member>
<member name="P:VirindiViewService.TooltipSystem.cTooltipInfo.Control">
<summary>
The HudControl that the tip is attached to.
</summary>
</member>
<member name="P:VirindiViewService.TooltipSystem.cTooltipInfo.Text">
<summary>
Deprecated.
Returns the text associated with a tooltip only if the tip contains a cStringRenderer.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudVScrollBar">
<summary>
A vertical scrollbar.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudHSlider">
<summary>
A horizontal slider.
</summary>
</member>
<member name="T:VirindiViewService.Controls.HudEmulator">
<summary>
A control that allows easy access to underlying draw methods.
</summary>
</member>
</members>
</doc>

View file

@ -1,3 +0,0 @@
<View>
<HudButton Name="btnCycle" Text="Cycle Gear" Location="10,10" Size="120,30" />
</View>

View file

@ -0,0 +1,264 @@
using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.Win32;
using Decal.Adapter;
namespace MosswartMassacre.Loader
{
[FriendlyName("MosswartMassacre.Loader")]
public class LoaderCore : FilterBase
{
private Assembly pluginAssembly;
private Type pluginType;
private object pluginInstance;
private FileSystemWatcher pluginWatcher;
private bool isSubscribedToRenderFrame = false;
private bool needsReload;
public static string PluginAssemblyNamespace => "MosswartMassacre";
public static string PluginAssemblyName => $"{PluginAssemblyNamespace}.dll";
public static string PluginAssemblyGuid => "{8C97E839-4D05-4A5F-B0C8-E8E778654322}";
public static bool IsPluginLoaded { get; private set; }
/// <summary>
/// Assembly directory (contains both loader and plugin dlls)
/// </summary>
public static string AssemblyDirectory => System.IO.Path.GetDirectoryName(Assembly.GetAssembly(typeof(LoaderCore)).Location);
public DateTime LastDllChange { get; private set; }
#region Event Handlers
protected override void Startup()
{
try
{
Core.PluginInitComplete += Core_PluginInitComplete;
Core.PluginTermComplete += Core_PluginTermComplete;
Core.FilterInitComplete += Core_FilterInitComplete;
// Set up assembly resolution for hot-loaded plugin dependencies
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
// watch the AssemblyDirectory for any .dll file changes
pluginWatcher = new FileSystemWatcher();
pluginWatcher.Path = AssemblyDirectory;
pluginWatcher.NotifyFilter = NotifyFilters.LastWrite;
pluginWatcher.Filter = "*.dll";
pluginWatcher.Changed += PluginWatcher_Changed;
pluginWatcher.EnableRaisingEvents = true;
}
catch (Exception ex)
{
Log(ex);
}
}
private void Core_FilterInitComplete(object sender, EventArgs e)
{
Core.EchoFilter.ClientDispatch += EchoFilter_ClientDispatch;
}
private void EchoFilter_ClientDispatch(object sender, NetworkMessageEventArgs e)
{
try
{
// Login_SendEnterWorldRequest
if (e.Message.Type == 0xF7C8)
{
//EnsurePluginIsDisabledInRegistry();
}
}
catch (Exception ex)
{
Log(ex);
}
}
private void Core_PluginInitComplete(object sender, EventArgs e)
{
try
{
LoadPluginAssembly();
}
catch (Exception ex)
{
Log(ex);
}
}
private void Core_PluginTermComplete(object sender, EventArgs e)
{
try
{
UnloadPluginAssembly();
}
catch (Exception ex)
{
Log(ex);
}
}
protected override void Shutdown()
{
try
{
Core.PluginInitComplete -= Core_PluginInitComplete;
Core.PluginTermComplete -= Core_PluginTermComplete;
Core.FilterInitComplete -= Core_FilterInitComplete;
AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
UnloadPluginAssembly();
}
catch (Exception ex)
{
Log(ex);
}
}
private void Core_RenderFrame(object sender, EventArgs e)
{
try
{
if (IsPluginLoaded && needsReload && DateTime.UtcNow - LastDllChange > TimeSpan.FromSeconds(1))
{
needsReload = false;
Core.RenderFrame -= Core_RenderFrame;
isSubscribedToRenderFrame = false;
LoadPluginAssembly();
}
}
catch (Exception ex)
{
Log(ex);
}
}
private void PluginWatcher_Changed(object sender, FileSystemEventArgs e)
{
try
{
// Only reload if it's the main plugin DLL
if (e.Name == PluginAssemblyName)
{
LastDllChange = DateTime.UtcNow;
needsReload = true;
if (!isSubscribedToRenderFrame)
{
isSubscribedToRenderFrame = true;
Core.RenderFrame += Core_RenderFrame;
}
}
}
catch (Exception ex)
{
Log(ex);
}
}
private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
try
{
// Extract just the assembly name (without version info)
string assemblyName = args.Name.Split(',')[0] + ".dll";
string assemblyPath = System.IO.Path.Combine(AssemblyDirectory, assemblyName);
// If the dependency exists in our plugin directory, load it
if (File.Exists(assemblyPath))
{
return Assembly.LoadFrom(assemblyPath);
}
}
catch (Exception ex)
{
Log($"AssemblyResolve failed for {args.Name}: {ex.Message}");
}
// Return null to let default resolution continue
return null;
}
#endregion
#region Plugin Loading/Unloading
internal void LoadPluginAssembly()
{
try
{
if (IsPluginLoaded)
{
UnloadPluginAssembly();
try
{
CoreManager.Current.Actions.AddChatText($"[MosswartMassacre] Reloading {PluginAssemblyName}", 5);
}
catch { }
}
pluginAssembly = Assembly.Load(File.ReadAllBytes(System.IO.Path.Combine(AssemblyDirectory, PluginAssemblyName)));
pluginType = pluginAssembly.GetType($"{PluginAssemblyNamespace}.PluginCore");
pluginInstance = Activator.CreateInstance(pluginType);
// Set the AssemblyDirectory property if it exists
var assemblyDirProperty = pluginType.GetProperty("AssemblyDirectory", BindingFlags.Public | BindingFlags.Static);
assemblyDirProperty?.SetValue(null, AssemblyDirectory);
// Set the IsHotReload flag if it exists
var isHotReloadProperty = pluginType.GetProperty("IsHotReload", BindingFlags.Public | BindingFlags.Static);
isHotReloadProperty?.SetValue(null, true);
// The original template doesn't set up Host - it just calls Startup
// The plugin should use CoreManager.Current.Actions instead of MyHost for hot reload scenarios
// We'll set a flag so the plugin knows it's being hot loaded
// Call Startup method
var startupMethod = pluginType.GetMethod("Startup", BindingFlags.NonPublic | BindingFlags.Instance);
startupMethod.Invoke(pluginInstance, new object[] { });
IsPluginLoaded = true;
}
catch (Exception ex)
{
Log(ex);
}
}
private void UnloadPluginAssembly()
{
try
{
if (pluginInstance != null && pluginType != null)
{
MethodInfo shutdownMethod = pluginType.GetMethod("Shutdown", BindingFlags.NonPublic | BindingFlags.Instance);
shutdownMethod.Invoke(pluginInstance, null);
pluginInstance = null;
pluginType = null;
pluginAssembly = null;
}
IsPluginLoaded = false;
}
catch (Exception ex)
{
Log(ex);
}
}
#endregion
private void Log(Exception ex)
{
Log(ex.ToString());
}
private void Log(string message)
{
File.AppendAllText(System.IO.Path.Combine(AssemblyDirectory, "loader_log.txt"), $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}\n");
try
{
CoreManager.Current.Actions.AddChatText($"[MosswartMassacre.Loader] {message}", 3);
}
catch { }
}
}
}

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<OutputType>Library</OutputType>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<PlatformTarget>x86</PlatformTarget>
<Version>1.0.0</Version>
<LangVersion>8</LangVersion>
<ProjectGuid>{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}</ProjectGuid>
<RootNamespace>MosswartMassacre.Loader</RootNamespace>
<AssemblyName>MosswartMassacre.Loader</AssemblyName>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<OutputPath>..\MosswartMassacre\bin\Debug\</OutputPath>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<OutputPath>..\MosswartMassacre\bin\Release\</OutputPath>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
</PropertyGroup>
<ItemGroup>
<Reference Include="Decal.Adapter">
<HintPath>..\MosswartMassacre\lib\Decal.Adapter.dll</HintPath>
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
<Reference Include="Decal.Interop.Core, Version=2.9.8.3, Culture=neutral, PublicKeyToken=481f17d392f1fb65, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<EmbedInteropTypes>False</EmbedInteropTypes>
<HintPath>..\..\..\..\..\..\Program Files (x86)\Decal 3.0\.NET 4.0 PIA\Decal.Interop.Core.DLL</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,3 @@
{
"enableAllProjectMcpServers": false
}

375
MosswartMassacre/CLAUDE.md Normal file
View file

@ -0,0 +1,375 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build Commands
Build the project:
```
msbuild MosswartMassacre.csproj /p:Configuration=Release /p:Platform=AnyCPU
```
Build debug version:
```
msbuild MosswartMassacre.csproj /p:Configuration=Debug /p:Platform=AnyCPU
```
Restore NuGet packages:
```
nuget restore packages.config
```
Generate installer (post-build, Windows only):
```
powershell -File scripts/post-build.ps1 -NuGetPackageRoot "%USERPROFILE%\.nuget\packages" -ProjectDir "./"
```
Build entire solution from parent directory:
```
msbuild ../mossy.sln
```
## Architecture Overview
This is a DECAL plugin for Asheron's Call that tracks monster kills and rare item discoveries. The plugin architecture consists of several key components:
### Core Plugin Framework
- **PluginCore.cs**: Main plugin entry point inheriting from `PluginBase`. Handles Startup/Shutdown lifecycle, event subscriptions (chat, spawn/despawn, login), and coordinates all subsystems.
- **PluginSettings.cs**: Per-character YAML configuration management using YamlDotNet. Settings are stored as `<CharacterName>.yaml` in the plugin directory.
### UI System (VVS Direct Integration)
- **Views/VVSTabbedMainView.cs**: Main tabbed UI using direct VirindiViewService integration. Displays kill stats, rare counts, elapsed time, settings, statistics, and navigation controls.
- **Views/VVSBaseView.cs**: Base class for VVS-based views providing common functionality like window positioning, control access, and resource management.
- **ViewXML/mainViewTabbed.xml**: XML layout definition for the tabbed UI overlay.
### Communication Systems
- **WebSocket.cs**: Bidirectional WebSocket client connecting to `wss://overlord.snakedesert.se/websocket/` for real-time data streaming (spawns, chat, rares) and remote command reception.
- **HttpCommandServer.cs**: Local HTTP server on port 8085 accepting POST commands for remote control.
- **Telemetry.cs**: Periodic HTTP POST of player stats/position to configured endpoint.
### Game Integration
- **vTank.cs + VtankControl.cs**: Interface with vTank automation bot for meta state control and **NAVIGATION CONTROL**
- **MossyInventory.cs**: Inventory change monitoring and logging.
- **DelayedCommandManager.cs**: Queue system for executing chat commands with delays.
### Utility Components
- **Coordinates.cs + Geometry.cs**: Game coordinate system handling and spatial calculations.
- **Utils.cs**: General utility functions for player position, distance calculations, etc.
- **ManyHook.cs**: Low-level Windows message hooking functionality.
### Event Processing Flow
1. **Chat Events**: `OnChatText()` in PluginCore.cs:217 parses kill messages using regex patterns, rare discovery messages, and remote commands from allegiance chat.
2. **World Events**: `OnSpawn()`/`OnDespawn()` in PluginCore.cs:150,171 track monster spawning for WebSocket streaming.
3. **Command Processing**: `/mm` command handler in `HandleMmCommand()` at PluginCore.cs:448 provides comprehensive plugin control interface.
### Important Implementation Notes
- All regex patterns for kill detection are in `IsKilledByMeMessage()` at PluginCore.cs:337
- Settings auto-save on shutdown and load after character login complete
- WebSocket uses session-based authentication with SharedSecret
- Plugin requires `AllowUnsafeBlocks=true` for P/Invoke operations
### Dependencies
- **DECAL Framework**: Core AC plugin framework (Decal.Adapter, Decal.Interop.Core, Decal.Interop.D3DService)
- **VirindiViewService**: UI system for game overlays - used directly without wrapper abstraction
- **Newtonsoft.Json**: JSON serialization for WebSocket/HTTP communication
- **YamlDotNet**: YAML configuration file handling
- **utank2-i.dll**: vTank integration library
### Key Configuration
- Target Framework: .NET Framework 4.8
- Platform: x86 (required for AC integration)
- Unsafe blocks enabled for low-level operations
- VVS direct integration (no wrapper abstraction layer)
- Post-build script generates NSIS installer
### Shared Code Structure
The project links to shared code in `../Shared/` containing common utilities for multiple AC plugins including constants, world object wrappers, settings management, and VCS chat system integration.
## Navigation Visualization Feature ✅ COMPLETED
### Overview
Successfully implemented complete navigation route comparison feature allowing visualization of VTank .nav files alongside UtilityBelt's active route visualization. Feature is **FULLY FUNCTIONAL** as of May 29, 2025.
### Key Components Added
1. **NavRoute.cs** - VTank nav file parser and D3D line visualizer
2. **NavVisualization.cs** - Route management and file discovery
3. **Enhanced TabbedMainView.cs** - Navigation tab with controls
4. **Navigation Tab UI** - Dropdown selection, enable/disable, status display
### Features Implemented
- ✅ Complete VTank .nav file format parsing (all waypoint types)
- ✅ 3D route visualization with red colored lines
- ✅ Support for all nav types: Circular, Linear, Target, Once
- ✅ Registry-based VTank directory detection
- ✅ Tabbed UI integration with existing interface
- ✅ Enable/disable visualization controls
- ✅ Route loading and clearing functionality
- ✅ Comprehensive debug logging system
### Usage Instructions
1. **Enable**: Check "Enable Navigation Visualization" in Navigation tab
2. **Configure**: Set VTank profiles path in Settings tab (auto-detected)
3. **Select**: Choose route from dropdown and click "Load Route"
4. **View**: Red lines appear in 3D game world showing route path
### Technical Details
- **Dependencies**: Added Decal.Interop.D3DService reference
- **Performance**: Optimized for large routes (max 500 line segments)
- **Integration**: Seamless addition to existing plugin architecture
- **Memory Management**: Proper D3D object disposal and cleanup
### Reference Documentation
See `NAVIGATION_IMPLEMENTATION_SUMMARY.md` for complete technical details, implementation notes, and future enhancement opportunities.
---
## GUI Architecture (Enhanced with Navigation Feature)
### Current Implementation Status
**Tabbed Interface**: Successfully implemented with Navigation, Settings, and Statistics tabs
**BaseView Architecture**: Inherited from UtilityBelt's BaseView pattern
**Advanced Controls**: Dropdown lists, checkboxes, buttons with proper event handling
**Settings Integration**: VTank path configuration in Settings tab
**Resource Management**: Proper disposal and memory management
### Tabs Implemented
1. **Main Tab**: Kill tracking functionality (original)
2. **Settings Tab**: Configuration including VTank profiles path
3. **Statistics Tab**: Enhanced statistics display
4. **Navigation Tab**: ✅ Route visualization controls (NEW)
### Future Enhancement Opportunities
1. **Multiple Route Support**: Visualize several routes simultaneously
2. **Route Analysis**: Distance calculations and efficiency metrics
3. **Advanced Visualization**: Waypoint icons and route optimization
4. **Export Features**: Save custom routes or export comparisons
## VTank Navigation Control Feature ✅ COMPLETED
### Overview
Successfully implemented complete programmatic control of VTank's navigation system, allowing remote waypoint advancement through the `/mm nextwp` command. Feature is **FULLY FUNCTIONAL** as of May 29, 2025.
### Breakthrough Achievement
Cracked VTank's internal architecture through reverse engineering and reflection to enable seamless integration between Mosswart Massacre and VTank's navigation system.
### Key Implementation
- **VtankControl.cs:114-223** - `VtAdvanceWaypoint()` method using precise reflection
- **PluginCore.cs:661-671** - `/mm nextwp` command handler with user feedback
- **Decompiled Analysis** - Complete VTank internal structure mapping
### Technical Solution
Resolved "Ambiguous match found" reflection error by specifying exact method signatures:
```csharp
Type[] parameterTypes = new Type[] { typeof(object), assembly.GetType("MetaViewWrappers.MVControlEventArgs") };
var advanceMethod = pluginCoreType.GetMethod("i",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance,
null, parameterTypes, null);
```
### Features Implemented
- ✅ Programmatic waypoint advancement via `/mm nextwp`
- ✅ Navigation state validation (waypoint count, position, nav type)
- ✅ VTank business logic compliance (Target/Once type restrictions)
- ✅ Comprehensive error handling and user feedback
- ✅ Fallback mechanisms for maximum reliability
- ✅ Assembly-based reflection for internal access
### Architecture Discovery
**VTank Internal Structure**:
```
vTank.Instance (cExternalInterfaceTrustedRelay)
└── Assembly: utank2-i.dll
└── Type: uTank2.PluginCore
├── Static Field: PC (PluginCore instance)
│ └── Method: i(object, MVControlEventArgs) ← TARGET
└── Static Field: dz (fallback direct access)
└── Field: o.l (current waypoint index)
```
### Usage
Simply type `/mm nextwp` in-game to advance VTank to the next waypoint. Works with Circular and Linear navigation types.
### Reference Documentation
See `FINDINGS.md` for complete technical breakthrough details, implementation analysis, and reverse engineering discoveries.
## Universal Plugin Message Interception via Harmony ✅ SUCCESSFULLY IMPLEMENTED
### Current Status: FULLY FUNCTIONAL - Harmony 1.2.0.1 Integration Complete
Successfully implemented **universal plugin message interception** using UtilityBelt's Harmony 1.2.0.1 DLL to patch DECAL's core chat methods. All plugin messages are now being captured and streamed!
### ✅ **IMPLEMENTATION SUCCESS**:
- ✅ **UtilityBelt Compatibility Resolved**: Using same Harmony 1.2.0.1 DLL as UB
- ✅ **Plugin Message Capture Working**: Successfully intercepting [UB], [VI], [VGI], [VTank] messages
- ✅ **WebSocket Streaming Active**: All intercepted messages stream to remote server
- ✅ **No Plugin Conflicts**: All plugins functioning normally with our patches
### 🎯 What's Working
- **Harmony Framework**: Complete DECAL method interception using Harmony 1.2.0.1 (old API)
- **Dual-Mode Patching**: Successfully patches `HooksWrapper.AddChatText()` and `AddChatTextRaw()`
- **Universal Capture**: Capturing plugin messages from UtilityBelt, VI, VGI, VTank
- **Debug System**: `/mm decalstatus`, `/mm decaldebug`, `/mm harmonyraw` commands
- **WebSocket Integration**: Real-time streaming of all intercepted messages
- **Duplicate Prevention**: Smart logic prevents double-patching same instance
### 🔧 Technical Solution Implemented
#### **Harmony Version Resolution**
- **Solution Applied**: Using UtilityBelt's 0Harmony.dll (version 1.2.0.1)
- **API Conversion**: Changed from Harmony 2.x API to Harmony 1.x API
- **Key Changes**:
- `HarmonyLib.Harmony``Harmony.HarmonyInstance`
- `new Harmony()``HarmonyInstance.Create()`
- Reference points to `C:\Games\Decal Plugins\UtilityBelt\0Harmony.dll`
#### **Current Patching Architecture**
1. **DecalHarmonyClean.cs** ✅ WORKING
- **Harmony 1.x Integration**: Uses `Harmony.HarmonyInstance` API
- **Smart Patching**: Avoids duplicates by checking if Host.Actions == HooksWrapper
- **Methods Patched**: `AddChatText(string, int)`, `AddChatText(string, int, int)`, `AddChatTextRaw` variants
- **Debug Logging**: Internal queue-based logging with `/mm harmonyraw` command
2. **Message Processing Flow**
- All messages (including [Mosswart Massacre]) are captured and streamed
- Debug output filtered to prevent spam but streaming remains active
- WebSocket streaming requires both `AggressiveChatStreamingEnabled` and `WebSocketEnabled`
### 📊 Performance & Results
- **Messages Intercepted**: Successfully capturing 100+ messages per session
- **Plugin Coverage**: [UB], [VI], [VGI], [VTank] messages all captured
- **Streaming Reliability**: All messages successfully sent to WebSocket endpoint
- **No Performance Impact**: Harmony patches execute efficiently
### ✅ **COMPLETED: Code Cleanup Plan - All Phases Done (May 31, 2025)**
**Successfully completed comprehensive codebase cleanup to resolve API JSON encoding errors and remove experimental code:**
#### **✅ Phase 1: Remove Unused Harmony Experiments** - COMPLETED
**Files Removed (Previously):**
- All experimental Harmony files removed in earlier cleanup sessions
- Kept: `DecalHarmonyClean.cs` (working implementation)
#### **✅ Phase 2: Remove Failed Direct Hook Attempts** - COMPLETED
**Files Removed (Previously):**
- All failed hook attempt files removed in earlier cleanup sessions
- Direct hooks, COM interceptors, and window monitoring approaches removed
#### **✅ Phase 3: Clean Debug & Test Commands** - COMPLETED
**Status:** All debug commands were already cleaned up, no obsolete commands found
#### **✅ Phase 4: Remove Rich Text Formatting** - COMPLETED
**API Error Resolution:**
- **Root Cause Found:** Emoji characters (🎯, ✅, ❌, 🔧, 🔍, 🔄) in chat messages were causing JSON encoding errors
- **Fixed:** All emoji replaced with plain text markers ([OK], [FAIL], [INFO], [WARN])
- **Files Updated:** `PluginCore.cs`, `VTankChatIntegration.cs`, `VCSIntegration.cs`, `PluginLoadDiagnostic.cs`
- **Result:** API JSON encoding errors resolved
#### **✅ Phase 5: Clean Project References** - COMPLETED
**Status:** Project references and packages.config were already clean, no unnecessary dependencies found
#### **✅ Phase 6: Consolidate Integration Classes** - COMPLETED
**Files Removed:**
- `UtilityBeltControl.cs` - Unused UtilityBelt integration (superseded by Harmony)
- `VTankChatIntegration.cs` - Unused VTank chat integration (superseded by Harmony)
- `VCSIntegration.cs` - Unused VCS integration (superseded by Harmony)
- **Project File Updated:** Removed references to deleted integration classes
#### **✅ Phase 7: Clean Verbose Debug Output** - COMPLETED
**Issue:** Harmony initialization was producing 20+ lines of debug spam in chat
**Resolution:**
- **DecalHarmonyClean.cs:** Removed all verbose `PluginCore.WriteToChat()` debug messages
- **PluginCore.cs:** Simplified Harmony initialization to single success/failure message
- **Result:** Clean startup with just `[OK] Plugin message interception active`
#### **✅ Phase 8: Remove Development Scaffolding** - COMPLETED
**Files Removed:**
- `PluginLoadDiagnostic.cs` - Development testing utility no longer needed
- **Syntax Errors Fixed:** Corrected string interpolation issues in `DecalHarmonyClean.cs`
- **Project File Updated:** Removed reference to diagnostic file
### 📋 **Final Testing Checklist**
1. ✅ `/mm decalstatus` - Verify Harmony patches still active
2. ✅ `/mm decaldebug enable` - Test plugin message capture
3. ✅ WebSocket streaming - Confirm no JSON encoding errors
4. ✅ Plugin compatibility - No conflicts with UtilityBelt/VTank
5. ✅ Build verification - All syntax errors resolved
6. ✅ Clean startup - No verbose debug spam, single initialization message
### 🏗️ Build Configuration
- **Harmony Reference**: `C:\Games\Decal Plugins\UtilityBelt\0Harmony.dll`
- **Copy Local**: False (uses UB's loaded assembly)
- **Target Framework**: .NET 4.8
- **Platform**: x86
## Flag Tracker Feature ✅ IN PROGRESS - VVS Integration
### Overview
Successfully implemented comprehensive Flag Tracker feature porting the complete flagtracker Lua plugin from UBS to C# using VirindiViewService (VVS). This is a major new feature providing character tracking for augmentations, luminance auras, recall spells, society quests, character flags, cantrips, and weapons.
### Implementation Status
**Core Framework Completed**:
- FlagTrackerView.cs - Complete 10-tab VVS window
- FlagTrackerData.cs - All data structures ported from Lua
- QuestManager.cs - Quest tracking system
- flagTracker.xml - Comprehensive UI layout
**Integration Completed**:
- Added Flag Tracker tab to main tabbed view
- Button launches dedicated Flag Tracker window
- VVS inheritance from VVSBaseView
- Proper event handling and resource management
### Current Issue: VVS HudList Column Access
**Problem**: "Column out of range" errors when populating VVS HudList controls
**Root Cause**: VVS HudList column creation from XML may not be working as expected
**Status**: Implementing safe column access patterns with detailed debugging
### Files Implemented
1. **Views/FlagTrackerView.cs** - Main Flag Tracker window
- 10-tab structure: Augs, Lum, Recalls, Society, FacHub, Flags, Cantrips, Weapons, Quests, Settings
- VVS HudList population methods with SafeSetListText() helper
- Complete event handling for all refresh buttons
2. **FlagTrackerData.cs** - Data management
- Complete augmentation data (Death, Skill, Rating, Stat, Resistance, Spec, Misc)
- Luminance aura categories (Nalicana, Seer)
- Recall spell definitions
- Society quest structures
- Character flag categories
- Cantrip and weapon data structures
- Reflection-based DECAL property access
3. **QuestManager.cs** - Quest tracking system
- DECAL CoreManager integration
- Quest refresh and parsing
4. **ViewXML/flagTracker.xml** - UI Layout
- 10-tab notebook control
- HudList controls with column definitions
- Comprehensive button and settings controls
### Technical Approach
- **VVS Direct Integration**: No wrapper abstraction, direct VirindiViewService usage
- **Safe Column Access**: SafeSetListText() method with IndexOutOfRangeException handling
- **Reflection-Based Data Access**: Bypasses DECAL type system issues using raw integer property access
- **Complete Data Preservation**: "No simplification or truncation" as requested by user
### Architecture Integration
- **Main View Integration**: Flag Tracker button added to mainViewTabbed.xml
- **Event Handling**: OnOpenFlagTrackerClick() in VVSTabbedMainView.cs
- **Static Interface**: OpenFlagTracker(), CloseFlagTracker(), IsOpen() methods
- **Resource Management**: Proper disposal in base VVSBaseView pattern
### Debug Features Added
- Control initialization status reporting
- Safe column access with detailed error messages
- Column existence validation before access
### Next Steps
1. Resolve VVS HudList column creation from XML
2. Complete remaining tab data population
3. Implement actual character data retrieval
4. Add settings persistence
---

View file

@ -0,0 +1,35 @@
using System.Diagnostics;
using System.Threading;
using System;
public class ClientTelemetry
{
private readonly Process _proc;
public ClientTelemetry()
{
_proc = Process.GetCurrentProcess();
}
/// <summary>Working-set memory in bytes.</summary>
public long MemoryBytes => _proc.WorkingSet64;
/// <summary>Total open handles.</summary>
public int HandleCount => _proc.HandleCount;
/// <summary>CPU utilisation (%) averaged over <paramref name="sampleMs"/>.</summary>
public float GetCpuUsage(int sampleMs = 500)
{
// you can keep your PerformanceCounter variant, but heres a simpler PID-based way:
var startCpu = _proc.TotalProcessorTime;
var start = DateTime.UtcNow;
Thread.Sleep(sampleMs);
var endCpu = _proc.TotalProcessorTime;
var end = DateTime.UtcNow;
// CPUtime used across all cores:
var cpuMs = (endCpu - startCpu).TotalMilliseconds;
var elapsedMs = (end - start).TotalMilliseconds * Environment.ProcessorCount;
return (float)(cpuMs / elapsedMs * 100.0);
}
}

View file

@ -0,0 +1,557 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Decal.Adapter;
using Decal.Adapter.Wrappers;
using Harmony; // UtilityBelt's Harmony 1.2.0.1 uses old Harmony namespace
namespace MosswartMassacre
{
/// <summary>
/// Clean Harmony implementation using UtilityBelt's exact pattern
/// Uses same namespace convention, same API, same Harmony package (Lib.Harmony 2.2.2)
/// </summary>
public static class DecalHarmonyClean
{
// Use UtilityBelt's namespace pattern
private static readonly string harmonyNamespace = "com.mosswartmassacre.decal";
private static HarmonyInstance harmonyDecal;
private static bool patchesApplied = false;
internal static int messagesIntercepted = 0;
// Debug logging for troubleshooting
private static readonly Queue<string> debugLog = new Queue<string>();
private const int MAX_DEBUG_ENTRIES = 50;
/// <summary>
/// Initialize Harmony patches using UtilityBelt's exact pattern
/// </summary>
public static bool Initialize()
{
try
{
// Use UtilityBelt's exact pattern: lazy initialization check + new Harmony()
if (harmonyDecal == null)
harmonyDecal = HarmonyInstance.Create(harmonyNamespace + ".patches");
// Apply patches using UtilityBelt's approach
ApplyDecalPatches();
return patchesApplied;
}
catch (Exception ex)
{
// Only log critical initialization failures
PluginCore.WriteToChat($"[DECAL] Critical initialization error: {ex.Message}");
return false;
}
}
/// <summary>
/// Apply patches to DECAL's AddChatText methods using UtilityBelt approach
/// </summary>
private static void ApplyDecalPatches()
{
try
{
// PATHWAY 1: Target HooksWrapper.AddChatText
PatchHooksWrapper();
// PATHWAY 2: Target Host.Actions.AddChatText (what our plugin uses)
PatchHostActions();
}
catch
{
// Only log if completely unable to apply any patches
}
}
/// <summary>
/// Patch HooksWrapper.AddChatText methods
/// </summary>
private static void PatchHooksWrapper()
{
try
{
Type hooksWrapperType = typeof(HooksWrapper);
var allAddChatTextMethods = hooksWrapperType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.Name == "AddChatText" || m.Name == "AddChatTextRaw").ToArray();
foreach (var method in allAddChatTextMethods)
{
var parameters = method.GetParameters();
string prefixMethodName = parameters.Length == 2 ? "AddChatTextPrefixHooks" :
parameters.Length == 3 ? "AddChatTextPrefixHooks3" :
"AddChatTextPrefixHooksGeneric";
try
{
ApplySinglePatch(method, prefixMethodName);
}
catch
{
}
}
}
catch
{
}
}
/// <summary>
/// Patch Host.Actions.AddChatText methods (what our plugin uses)
/// </summary>
private static void PatchHostActions()
{
try
{
if (PluginCore.MyHost?.Actions == null)
{
return;
}
Type actionsType = PluginCore.MyHost.Actions.GetType();
// Check if Host.Actions is already a HooksWrapper (to avoid double patching)
if (actionsType == typeof(HooksWrapper))
{
// PATHWAY 3: Try to patch at PluginHost level
PatchPluginHost();
return;
}
var hostAddChatTextMethods = actionsType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.Name == "AddChatText" || m.Name == "AddChatTextRaw").ToArray();
foreach (var method in hostAddChatTextMethods)
{
var parameters = method.GetParameters();
string prefixMethodName = parameters.Length == 3 ? "AddChatTextPrefixHost" :
parameters.Length == 4 ? "AddChatTextPrefixHost4" :
"AddChatTextPrefixHostGeneric";
try
{
ApplySinglePatch(method, prefixMethodName);
}
catch
{
}
}
// PATHWAY 3: Try to patch at PluginHost level
PatchPluginHost();
}
catch
{
}
}
/// <summary>
/// Try a different approach - patch the actual chat system or find global instances
/// </summary>
private static void PatchPluginHost()
{
try
{
// Try to patch CoreManager.Current.Actions if it's different
try
{
var coreActions = CoreManager.Current?.Actions;
if (coreActions != null && coreActions != PluginCore.MyHost?.Actions)
{
var coreActionsMethods = coreActions.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.Name == "AddChatText" || m.Name == "AddChatTextRaw" || m.Name == "AddStatusText").ToArray();
foreach (var method in coreActionsMethods)
{
var parameters = method.GetParameters();
try
{
string prefixMethodName = "AddChatTextPrefixCore" + parameters.Length;
ApplySinglePatch(method, prefixMethodName);
}
catch
{
}
}
}
}
catch
{
}
}
catch
{
}
}
/// <summary>
/// Apply a single patch using UtilityBelt's method
/// </summary>
private static void ApplySinglePatch(MethodInfo targetMethod, string prefixMethodName)
{
try
{
// Get our prefix method
var prefixMethod = typeof(DecalPatchMethods).GetMethod(prefixMethodName,
BindingFlags.Static | BindingFlags.Public);
if (prefixMethod != null)
{
// Use UtilityBelt's exact approach
harmonyDecal.Patch(targetMethod, new HarmonyMethod(prefixMethod));
patchesApplied = true;
}
}
catch
{
}
}
/// <summary>
/// List available methods for debugging
/// </summary>
private static void ListAvailableMethods(Type type)
{
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.Name == "AddChatText").ToArray();
foreach (var method in methods)
{
var parameters = method.GetParameters();
string paramInfo = string.Join(", ", parameters.Select(p => p.ParameterType.Name));
}
}
/// <summary>
/// Clean up patches using UtilityBelt's pattern
/// </summary>
public static void Cleanup()
{
try
{
if (harmonyDecal != null && patchesApplied)
{
// Use UtilityBelt's cleanup pattern
harmonyDecal.UnpatchAll(harmonyNamespace + ".patches");
}
patchesApplied = false;
}
catch
{
}
}
/// <summary>
/// Status checks
/// </summary>
public static bool IsActive() => patchesApplied && harmonyDecal != null;
public static int GetMessagesIntercepted() => messagesIntercepted;
/// <summary>
/// Add debug log entry
/// </summary>
public static void AddDebugLog(string message)
{
lock (debugLog)
{
debugLog.Enqueue($"{DateTime.Now:HH:mm:ss.fff} {message}");
while (debugLog.Count > MAX_DEBUG_ENTRIES)
{
debugLog.Dequeue();
}
}
}
/// <summary>
/// Get debug log entries
/// </summary>
public static string[] GetDebugLog()
{
lock (debugLog)
{
return debugLog.ToArray();
}
}
}
/// <summary>
/// Patch methods for DECAL interception using UtilityBelt's approach
/// </summary>
public static class DecalPatchMethods
{
/// <summary>
/// Prefix method for HooksWrapper.AddChatText(string, int) - intercepts plugin messages via HooksWrapper
/// </summary>
public static bool AddChatTextPrefixHooks(string text, int color)
{
try
{
// Always increment to verify patch is working
DecalHarmonyClean.messagesIntercepted++;
if (PluginCore.AggressiveChatStreamingEnabled)
{
}
// Process ALL messages (including our own) for streaming
if (!string.IsNullOrEmpty(text))
{
// Process the intercepted message
ProcessInterceptedMessage(text, color, "HooksWrapper-2param");
// Silent operation - debug output removed
}
// Always return true to let the original AddChatText continue
return true;
}
catch
{
// Never let our interception break other plugins
return true;
}
}
/// <summary>
/// Prefix method for HooksWrapper.AddChatText(string, int, int) - 3-parameter version
/// </summary>
public static bool AddChatTextPrefixHooks3(string text, int color, int target)
{
try
{
DecalHarmonyClean.messagesIntercepted++;
if (!string.IsNullOrEmpty(text))
{
ProcessInterceptedMessage(text, color, $"HooksWrapper-3param(target={target})");
// Silent operation - debug output removed
}
return true;
}
catch
{
return true;
}
}
/// <summary>
/// Generic prefix method for any other HooksWrapper AddChatText overloads
/// </summary>
public static bool AddChatTextPrefixHooksGeneric(string text)
{
try
{
DecalHarmonyClean.messagesIntercepted++;
if (!string.IsNullOrEmpty(text))
{
ProcessInterceptedMessage(text, 0, "HooksWrapper-generic");
// Silent operation - debug output removed
}
return true;
}
catch
{
return true;
}
}
/// <summary>
/// Process intercepted plugin messages
/// </summary>
private static void ProcessInterceptedMessage(string text, int color, string source)
{
try
{
// Identify source plugin
string sourcePlugin = IdentifySourcePlugin(text);
// Add timestamp
var timestamp = DateTime.Now.ToString("HH:mm:ss");
var fullMessage = $"{timestamp} [{sourcePlugin}] {text}";
// Debug logging
// Stream to WebSocket if both debug streaming AND WebSocket are enabled
if (PluginCore.AggressiveChatStreamingEnabled && PluginCore.WebSocketEnabled)
{
Task.Run(() => WebSocket.SendChatTextAsync(color, text));
}
}
catch
{
}
}
/// <summary>
/// Identify which plugin sent the message using UtilityBelt's patterns
/// </summary>
private static string IdentifySourcePlugin(string text)
{
// Known plugin prefixes
if (text.StartsWith("[VTank]")) return "VTank";
if (text.StartsWith("[UB]") || text.Contains("UtilityBelt")) return "UtilityBelt";
if (text.StartsWith("[VGI]")) return "VGI";
if (text.StartsWith("[VI]")) return "VirindiIntegrator";
if (text.StartsWith("[GoArrow]")) return "GoArrow";
if (text.StartsWith("[Meta]")) return "Meta";
if (text.StartsWith("[VTClassic]")) return "VTClassic";
// Pattern-based detection for messages without prefixes
if (text.Contains("Macro started") || text.Contains("Macro stopped")) return "VTank";
if (text.Contains("Quest data updated")) return "UtilityBelt";
if (text.Contains("Database read complete")) return "VGI";
if (text.Contains("Disconnected, retry")) return "VI";
return "Unknown";
}
// ===== HOST.ACTIONS PREFIX METHODS =====
/// <summary>
/// Prefix method for Host.Actions.AddChatText(string, int, int) - 3-parameter version
/// </summary>
public static bool AddChatTextPrefixHost(string text, int color, int target)
{
try
{
DecalHarmonyClean.messagesIntercepted++;
if (PluginCore.AggressiveChatStreamingEnabled)
{
}
if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]"))
{
ProcessInterceptedMessage(text, color, $"Host.Actions-3param(target={target})");
// Silent operation - debug output removed
}
return true;
}
catch
{
return true;
}
}
/// <summary>
/// Prefix method for Host.Actions.AddChatText(string, int, int, int) - 4-parameter version
/// </summary>
public static bool AddChatTextPrefixHost4(string text, int color, int target, int window)
{
try
{
DecalHarmonyClean.messagesIntercepted++;
if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]"))
{
ProcessInterceptedMessage(text, color, $"Host.Actions-4param(target={target},window={window})");
// Silent operation - debug output removed
}
return true;
}
catch
{
return true;
}
}
/// <summary>
/// Generic prefix method for any other Host.Actions.AddChatText overloads
/// </summary>
public static bool AddChatTextPrefixHostGeneric(string text)
{
try
{
DecalHarmonyClean.messagesIntercepted++;
if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]"))
{
ProcessInterceptedMessage(text, 0, "Host.Actions-generic");
// Silent operation - debug output removed
}
return true;
}
catch
{
return true;
}
}
// ===== COREMANAGER.ACTIONS PREFIX METHODS =====
/// <summary>
/// Prefix method for CoreManager.Actions methods - trying different instances
/// </summary>
public static bool AddChatTextPrefixCore2(string text, int color)
{
try
{
DecalHarmonyClean.messagesIntercepted++;
if (PluginCore.AggressiveChatStreamingEnabled)
{
}
if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]"))
{
ProcessInterceptedMessage(text, color, "CoreActions-2param");
// Silent operation - debug output removed
}
return true;
}
catch
{
return true;
}
}
/// <summary>
/// Prefix method for CoreManager.Actions 3-param methods
/// </summary>
public static bool AddChatTextPrefixCore3(string text, int color, int target)
{
try
{
DecalHarmonyClean.messagesIntercepted++;
if (PluginCore.AggressiveChatStreamingEnabled)
{
}
if (!string.IsNullOrEmpty(text) && !text.Contains("[Mosswart Massacre]"))
{
ProcessInterceptedMessage(text, color, $"CoreActions-3param(target={target})");
// Silent operation - debug output removed
}
return true;
}
catch
{
return true;
}
}
}
}

View file

@ -28,8 +28,8 @@ namespace MosswartMassacre
{
while (delayedCommands.Count > 0 && delayedCommands[0].RunAt <= DateTime.UtcNow)
{
PluginCore.WriteToChat($"[Debug] Executing delayed: {delayedCommands[0].Command}");
CoreManager.Current.Actions.InvokeChatParser(delayedCommands[0].Command);
// Use Decal_DispatchOnChatCommand to ensure other plugins can intercept
PluginCore.DispatchChatToBoxWithPluginIntercept(delayedCommands[0].Command);
delayedCommands.RemoveAt(0);
}

View file

@ -0,0 +1,304 @@
# VTank Next Waypoint Control - BREAKTHROUGH FINDINGS
## SUCCESS: VTank Waypoint Advancement WORKING ✅
**Date**: May 29, 2025
**Status**: FULLY FUNCTIONAL
**Command**: `/mm nextwp`
## The Challenge
Implementing programmatic control of VTank's "Advance Current Point" button functionality to skip to the next waypoint in a navigation route. This required deep reverse engineering of VTank's internal architecture.
## Key Technical Breakthrough
### The "Ambiguous Match Found" Problem
The critical issue was that VTank's `PluginCore` class contains multiple methods named `i`, causing reflection to fail with "Ambiguous match found" when trying to invoke the waypoint advance method.
### The Solution
Specified exact method signature using parameter types in the `GetMethod()` call:
```csharp
Type[] parameterTypes = new Type[] { typeof(object), assembly.GetType("MetaViewWrappers.MVControlEventArgs") };
var advanceMethod = pluginCoreType.GetMethod("i",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance,
null, parameterTypes, null);
```
## Complete VTank Architecture Discovery
### Core Architecture Components
#### 1. External Interface Layer
- **Type**: `cExternalInterfaceTrustedRelay`
- **Access**: `vTank.Instance` (singleton pattern)
- **Purpose**: Safe public API facade with permission checking
- **Assembly**: `utank2-i.dll`
- **Permission Levels**: Uses `eExternalsPermissionLevel` enum for access control
#### 2. Internal Core Logic
- **Type**: `uTank2.PluginCore`
- **Static Instance**: `PC` field (PluginCore singleton)
- **Purpose**: Main plugin implementation containing all core functionality
- **Access Pattern**: External interface delegates to internal PluginCore methods
#### 3. Navigation System Architecture
```
Navigation Data Flow:
ExternalInterface.NavCurrent
└── calls: a(eExternalsPermissionLevel.FullUnderlying)
└── returns: PC.NavCurrent
└── accesses: dz.o.l (actual waypoint index storage)
```
### Deep Navigation Implementation Details
#### Navigation State Management
- **Current Waypoint**: `dz.o.l` (int field chain)
- **Total Waypoints**: `dz.o` contains waypoint collection
- **Navigation Type**: Enum values (0=Circular, 1=Linear, 2=Target, 4=Once)
- **Meta State Integration**: `dz.at.f()` and `dz.at.b(value)` for meta state control
#### Waypoint Advancement Logic (from decompiled source)
```csharp
// VTank's internal advance logic:
if (navType != 2 && navType != 4) // Not Target or Once
{
if (dz.o.l < totalWaypoints - 1) // Not at end
{
dz.o.l++; // Advance waypoint
}
}
```
#### Button Click Handler Chain
1. **UI Button**: "Advance Current Point" in VTank interface
2. **Event Handler**: `i(object A_0, MVControlEventArgs A_1)` method
3. **Permission Check**: Validates navigation state and type
4. **Core Logic**: Calls internal waypoint advancement
5. **State Update**: Updates `dz.o.l` and refreshes UI
### Permission and Security Model
#### Access Levels (eExternalsPermissionLevel)
- **LogicObject**: Basic access to cLogic object
- **FullUnderlying**: Complete access to internal state (required for navigation)
- **Permission Validation**: `a(level)` method checks permissions before access
#### Trust Relationship
- External plugins must be "trusted" to access navigation functions
- VTank validates calling assembly and permission levels
- Reflection bypasses normal permission checks (security consideration)
### Method Signature Discovery
#### Navigation Methods in PluginCore
- **Multiple 'i' Methods**: Caused "Ambiguous match found" error
- **Target Method**: `i(object A_0, MVControlEventArgs A_1)` - button event handler
- **Parameter Types**:
- `object` - sender/source object (can be null)
- `MetaViewWrappers.MVControlEventArgs` - VVS event args (can be null)
#### Reflection Access Pattern
```csharp
// Critical discovery: Must specify exact parameter types
Type[] parameterTypes = new Type[] {
typeof(object),
assembly.GetType("MetaViewWrappers.MVControlEventArgs")
};
var method = type.GetMethod("i", flags, null, parameterTypes, null);
```
### Navigation Type Behaviors
#### Circular Navigation (Type 0)
- ✅ Advance allowed at any waypoint
- Wraps from last waypoint back to first
- Most common navigation pattern
#### Linear Navigation (Type 1)
- ✅ Advance allowed until last waypoint
- Stops at final waypoint (no wrap-around)
- Used for point-to-point routes
#### Target Navigation (Type 2)
- ❌ Advance blocked by design
- Single destination waypoint
- Navigation automatically stops at target
#### Once Navigation (Type 4)
- ❌ Advance blocked by design
- One-time route execution
- Cannot manually advance waypoints
### Meta State Integration
#### Current Meta State Access
- **Getter**: `dz.at.f()` returns current meta state string
- **Setter**: `dz.at.b(value)` sets new meta state
- **Integration**: Navigation events can trigger meta state changes
- **Example**: Rare discoveries trigger "loot_rare" meta state
### Assembly and Type Discovery
#### Key Assembly Information
- **File**: `utank2-i.dll` (interface assembly)
- **Namespace**: `uTank2`
- **Main Type**: `PluginCore`
- **Static Fields**: `PC` (PluginCore instance), `dz` (navigation data)
#### Critical Type Relationships
```
uTank2.PluginCore (main logic)
├── Static PC: PluginCore instance
├── Static dz: Navigation data container
│ └── Field o: Navigation object
│ └── Field l: Current waypoint index (int)
└── Method i(object, MVControlEventArgs): Advance handler
```
### Error Patterns and Solutions
#### Common Reflection Errors
1. **"Ambiguous match found"**: Multiple methods with same name
- **Solution**: Specify exact parameter types in GetMethod()
2. **Permission Denied**: External interface blocks access
- **Solution**: Use assembly-level reflection to bypass interface
3. **Null Reference**: Static fields not initialized
- **Solution**: Validate object hierarchy before access
#### Debugging Techniques Used
1. **Type Inspection**: `GetType().Name` to identify actual types
2. **Assembly Analysis**: Loading and inspecting utank2-i.dll
3. **Field Enumeration**: Discovering static fields via reflection
4. **Method Signature Analysis**: Identifying parameter requirements
### Access Pattern Hierarchy
```
Application Level:
/mm nextwp command
└── VtankControl.VtAdvanceWaypoint()
└── Validation (waypoints, nav type, position)
└── Assembly.GetType("uTank2.PluginCore")
└── GetField("PC").GetValue(null)
└── GetMethod("i", paramTypes).Invoke(instance, params)
└── VTank Internal Navigation Logic
└── dz.o.l++ (waypoint advancement)
```
### Performance Characteristics
- **Reflection Overhead**: Minimal (only on command execution)
- **Memory Impact**: No persistent references to VTank objects
- **UI Responsiveness**: No blocking operations
- **Error Recovery**: Graceful fallback to direct field manipulation
## Implementation Details
### File: VtankControl.cs:114-223
Added `VtAdvanceWaypoint()` method with:
- Navigation validation (waypoint count, current position, nav type)
- Assembly-based reflection to access internal VTank types
- Primary approach: Method invocation via PluginCore.PC.i()
- Fallback approach: Direct field manipulation of dz.o.l
- Comprehensive error handling and user feedback
### File: PluginCore.cs:661-671
Added `/mm nextwp` command handler with success/failure messaging.
## Navigation Type Restrictions
Correctly implemented VTank's business logic:
- **Circular/Linear**: ✅ Advance allowed
- **Target/Once**: ❌ Advance blocked (matches VTank UI behavior)
## Testing Results
- ✅ Successfully advances waypoint in Circular navigation
- ✅ Properly validates navigation state before advancing
- ✅ Provides clear user feedback on success/failure
- ✅ Handles edge cases (no waypoints, at end of route)
- ✅ Respects VTank's navigation type restrictions
## Technical Lessons Learned
1. **Reflection Precision**: When dealing with obfuscated/complex assemblies, always specify exact method signatures using parameter types to avoid ambiguity.
2. **Assembly Navigation**: External interfaces often provide facade access to internal implementations via assembly type lookups.
3. **Static Field Patterns**: Many plugin architectures use static fields (like `PC`) to maintain singleton instances for cross-component access.
4. **Decompiled Code Value**: Reverse engineering provided exact field names (`dz.o.l`) and method signatures crucial for implementation.
5. **Fallback Strategies**: Implementing multiple access approaches (method invocation + direct field access) increases reliability.
## Future Enhancement Opportunities
1. **Previous Waypoint**: Implement `/mm prevwp` command using similar reflection techniques
2. **Jump to Waypoint**: `/mm gotowp <index>` for direct waypoint targeting
3. **Route Management**: Integration with VTank's route loading/saving functionality
4. **Navigation State Monitoring**: Real-time tracking of waypoint advancement
## Security Considerations
- Uses private reflection to access VTank internals
- Requires appropriate permissions and trust levels
- No modification of VTank files or persistent state
- Read-only access to navigation configuration
## Chat Capture System - CRITICAL LOOP PREVENTION ⚠️
### The Recursive Loop Problem
When implementing unified chat capture (native + plugin-generated), there's a serious risk of infinite recursion:
1. Plugin calls `WriteToChat()` → Adds text to chat window
2. Window monitor detects new text → Fires chat event
3. Chat handler encounters error → Calls `WriteToChat()` again
4. **INFINITE LOOP** leading to stack overflow and crash
### Prevention Mechanisms Implemented
#### 1. Message Filtering
```csharp
// Block our own plugin messages from being processed
if (text.Contains("[Mosswart Massacre]")) return false;
if (text.Contains("[UnifiedChat]")) return false;
// ... other plugin prefixes
```
#### 2. Safe Error Handling
```csharp
catch (Exception ex)
{
// NEVER use WriteToChat in error handlers!
System.Diagnostics.Debug.WriteLine($"Error: {ex}"); // Safe alternative
}
```
#### 3. Recursion Detection
```csharp
private static bool DetectPotentialRecursion()
{
// Monitor call frequency - if >10 calls in 100ms, likely a loop
if (_recursionDetectionCount > 10) return true;
}
```
#### 4. Multiple Safety Layers
- **Content filtering**: Block plugin-generated messages
- **Debug logging**: Use `Debug.WriteLine()` instead of `WriteToChat()` in handlers
- **Frequency monitoring**: Detect rapid-fire calls indicating loops
- **Circuit breaker**: Temporarily disable monitor if recursion detected
### Implementation Notes
- **NEVER** call `WriteToChat()` from within chat event handlers
- **ALWAYS** filter out your own plugin's messages
- **USE** `System.Diagnostics.Debug.WriteLine()` for error logging in chat handlers
- **TEST** thoroughly with `/mm chattest` command
This prevention system is **CRITICAL** for stability - without it, the plugin would crash AC client on any WebSocket error.
## Performance Impact
- Minimal: Reflection calls only on command execution
- No continuous polling or background processing
- Lightweight validation checks before expensive reflection operations
---
**CONCLUSION**: Complete programmatic control of VTank navigation achieved through precise reflection engineering. The `/mm nextwp` command now provides seamless integration between Mosswart Massacre and VTank's navigation system.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Costura>
<IncludeAssemblies>
YamlDotNet
Newtonsoft.Json
</IncludeAssemblies>
</Costura>
</Weavers>

View file

@ -1,96 +0,0 @@
using System;
using MyClasses.MetaViewWrappers;
namespace MosswartMassacre
{
internal static class MainView
{
private static IView View;
private static IStaticText lblTotalKills;
private static IStaticText lblKillsPer5Min;
private static IStaticText lblKillsPerHour;
private static IStaticText lblElapsedTime;
private static IStaticText lblRareCount;
private static IButton btnRestart;
private static IButton btnToggleRareMeta;
public static void ViewInit()
{
try
{
// Load the view from the embedded XML resource
View = MyClasses.MetaViewWrappers.ViewSystemSelector.CreateViewResource(
PluginCore.MyHost, "MosswartMassacre.ViewXML.mainView.xml");
// Get references to controls
lblTotalKills = (IStaticText)View["lblTotalKills"];
lblKillsPer5Min = (IStaticText)View["lblKillsPer5Min"];
lblKillsPerHour = (IStaticText)View["lblKillsPerHour"];
lblElapsedTime = (IStaticText)View["lblElapsedTime"];
lblRareCount = (IStaticText)View["lblRareCount"];
btnRestart = (IButton)View["btnRestart"];
btnRestart.Hit += OnRestartClick;
btnToggleRareMeta = (IButton)View["btnToggleRareMeta"];
btnToggleRareMeta.Hit += OnToggleRareMetaClick;
btnToggleRareMeta.Text = "Meta: ON";
PluginCore.WriteToChat("View initialized.");
}
catch (Exception ex)
{
PluginCore.WriteToChat("Error initializing view: " + ex.Message);
}
}
public static void ViewDestroy()
{
try
{
View.Dispose();
PluginCore.WriteToChat("View destroyed.");
btnRestart.Hit -= OnRestartClick;
btnToggleRareMeta.Hit -= OnToggleRareMetaClick;
}
catch (Exception ex)
{
PluginCore.WriteToChat("Error destroying view: " + ex.Message);
}
}
public static void UpdateKillStats(int totalKills, double killsPer5Min, double killsPerHour)
{
lblTotalKills.Text = $"Total Kills: {totalKills}";
lblKillsPer5Min.Text = $"Kills per 5 Min: {killsPer5Min:F2}";
lblKillsPerHour.Text = $"Kills per Hour: {killsPerHour:F2}";
}
public static void UpdateElapsedTime(TimeSpan elapsed)
{
int days = elapsed.Days;
int hours = elapsed.Hours;
int minutes = elapsed.Minutes;
int seconds = elapsed.Seconds;
if (days > 0)
lblElapsedTime.Text = $"Time: {days}d {hours:D2}:{minutes:D2}:{seconds:D2}";
else
lblElapsedTime.Text = $"Time: {hours:D2}:{minutes:D2}:{seconds:D2}";
}
public static void UpdateRareCount(int rareCount)
{
lblRareCount.Text = $"Rare Count: {rareCount}";
}
private static void OnRestartClick(object sender, EventArgs e)
{
PluginCore.RestartStats();
}
private static void OnToggleRareMetaClick(object sender, EventArgs e)
{
PluginCore.ToggleRareMeta();
}
public static void SetRareMetaToggleState(bool enabled)
{
btnToggleRareMeta.Text = enabled ? "Meta: ON" : "Meta: OFF";
}
}
}

View file

@ -29,25 +29,45 @@
<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>
<ItemGroup>
<Reference Include="0Harmony">
<HintPath>C:\Games\Decal Plugins\UtilityBelt\0Harmony.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Decal.Adapter">
<HintPath>lib\Decal.Adapter.dll</HintPath>
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
<Reference Include="Decal.FileService">
<HintPath>..\..\..\..\..\..\Program Files (x86)\Decal 3.0\Decal.FileService.dll</HintPath>
</Reference>
<Reference Include="Decal.Interop.Core, Version=2.9.8.3, Culture=neutral, PublicKeyToken=481f17d392f1fb65, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<EmbedInteropTypes>False</EmbedInteropTypes>
<HintPath>lib\Decal.Interop.Core.DLL</HintPath>
<HintPath>..\..\..\..\..\..\Program Files (x86)\Decal 3.0\.NET 4.0 PIA\Decal.Interop.Core.DLL</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Decal.Interop.Filters, Version=2.9.8.3, Culture=neutral, PublicKeyToken=481f17d392f1fb65, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<EmbedInteropTypes>False</EmbedInteropTypes>
<HintPath>..\..\..\..\..\..\Program Files (x86)\Decal 3.0\.NET 4.0 PIA\Decal.Interop.Filters.DLL</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Decal.Interop.Inject, Version=2.9.8.3, Culture=neutral, PublicKeyToken=481f17d392f1fb65, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<EmbedInteropTypes>False</EmbedInteropTypes>
<HintPath>lib\Decal.Interop.Inject.dll</HintPath>
</Reference>
<Reference Include="Decal.Interop.D3DService, Version=2.9.8.3, Culture=neutral, PublicKeyToken=481f17d392f1fb65, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<EmbedInteropTypes>False</EmbedInteropTypes>
<HintPath>..\..\..\..\..\..\Program Files (x86)\Decal 3.0\.NET 4.0 PIA\Decal.Interop.D3DService.DLL</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
@ -55,6 +75,7 @@
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Numerics" />
<Reference Include="System.Runtime.Remoting" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
@ -66,6 +87,9 @@
<SpecificVersion>False</SpecificVersion>
<HintPath>bin\Debug\utank2-i.dll</HintPath>
</Reference>
<Reference Include="VCS5">
<HintPath>..\..\..\..\..\..\Games\Decal Plugins\Virindi\VirindiChatSystem5\VCS5.dll</HintPath>
</Reference>
<Reference Include="VirindiViewService">
<HintPath>lib\VirindiViewService.dll</HintPath>
</Reference>
@ -74,6 +98,76 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="..\Shared\Constants\BoolValueKey.cs">
<Link>Shared\Constants\BoolValueKey.cs</Link>
</Compile>
<Compile Include="..\Shared\Constants\Dictionaries.cs">
<Link>Shared\Constants\Dictionaries.cs</Link>
</Compile>
<Compile Include="..\Shared\Spells\Spell.cs">
<Link>Shared\Spells\Spell.cs</Link>
</Compile>
<Compile Include="..\Shared\Constants\DoubleValueKey.cs">
<Link>Shared\Constants\DoubleValueKey.cs</Link>
</Compile>
<Compile Include="..\Shared\Constants\EphemeralAttribute.cs">
<Link>Shared\Constants\EphemeralAttribute.cs</Link>
</Compile>
<Compile Include="..\Shared\Constants\IntValueKey.cs">
<Link>Shared\Constants\IntValueKey.cs</Link>
</Compile>
<Compile Include="..\Shared\Constants\SendOnLoginAttribute.cs">
<Link>Shared\Constants\SendOnLoginAttribute.cs</Link>
</Compile>
<Compile Include="..\Shared\Constants\ServerOnlyAttribute.cs">
<Link>Shared\Constants\ServerOnlyAttribute.cs</Link>
</Compile>
<Compile Include="..\Shared\Constants\StringValueKey.cs">
<Link>Shared\Constants\StringValueKey.cs</Link>
</Compile>
<Compile Include="..\Shared\Debug.cs">
<Link>Shared\Debug.cs</Link>
</Compile>
<Compile Include="..\Shared\DecalProxy.cs">
<Link>Shared\DecalProxy.cs</Link>
</Compile>
<Compile Include="..\Shared\MyWorldObject.cs">
<Link>Shared\MyWorldObject.cs</Link>
</Compile>
<Compile Include="..\Shared\MyWorldObjectCreator.cs">
<Link>Shared\MyWorldObjectCreator.cs</Link>
</Compile>
<Compile Include="..\Shared\PostMessageTools.cs">
<Link>Shared\PostMessageTools.cs</Link>
</Compile>
<Compile Include="..\Shared\RateLimiter.cs">
<Link>Shared\RateLimiter.cs</Link>
</Compile>
<Compile Include="..\Shared\SerializableDictionary.cs">
<Link>Shared\SerializableDictionary.cs</Link>
</Compile>
<Compile Include="..\Shared\Settings\Setting.cs">
<Link>Shared\Settings\Setting.cs</Link>
</Compile>
<Compile Include="..\Shared\Settings\SettingsFile.cs">
<Link>Shared\Settings\SettingsFile.cs</Link>
</Compile>
<Compile Include="..\Shared\User32.cs">
<Link>Shared\User32.cs</Link>
</Compile>
<Compile Include="..\Shared\Util.cs">
<Link>Shared\Util.cs</Link>
</Compile>
<Compile Include="..\Shared\VCS_Connector.cs">
<Link>Shared\VCS_Connector.cs</Link>
</Compile>
<Compile Include="ClientTelemetry.cs" />
<Compile Include="DecalHarmonyClean.cs" />
<Compile Include="FlagTrackerData.cs" />
<Compile Include="MossyInventory.cs" />
<Compile Include="NavRoute.cs" />
<Compile Include="NavVisualization.cs" />
<Compile Include="QuestManager.cs" />
<Compile Include="vTank.cs" />
<Compile Include="VtankControl.cs" />
<Compile Include="Telemetry.cs" />
@ -83,19 +177,20 @@
<Compile Include="PluginSettings.cs" />
<Compile Include="HttpCommandServer.cs" />
<Compile Include="DelayedCommandManager.cs" />
<Compile Include="MainView.cs" />
<Compile Include="PluginCore.cs" />
<Compile Include="QuestNames.cs" />
<Compile Include="UpdateManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="ViewSystemSelector.cs" />
<Compile Include="Wrapper.cs" />
<Compile Include="Wrapper_Decal.cs" />
<Compile Include="Wrapper_MyHuds.cs" />
<Compile Include="Wrapper_WireupHelper.cs" />
<Compile Include="SpellManager.cs" />
<Compile Include="Views\FlagTrackerView.cs" />
<Compile Include="Views\VVSBaseView.cs" />
<Compile Include="Views\VVSTabbedMainView.cs" />
<Compile Include="WebSocket.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Properties\Resources.resx">
@ -104,11 +199,45 @@
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="ViewXML\flagTracker.xml" />
<EmbeddedResource Include="ViewXML\mainView.xml" />
<EmbeddedResource Include="ViewXML\mainViewTabbed.xml" />
<EmbeddedResource Include="..\Shared\Spells\Spells.csv">
<Link>Shared\Spells\Spells.csv</Link>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<COMReference Include="Decal">
<Guid>{FF7F5F6D-34E0-4B6F-B3BB-8141DE2EF732}</Guid>
<VersionMajor>2</VersionMajor>
<VersionMinor>0</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>primary</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>False</EmbedInteropTypes>
</COMReference>
<COMReference Include="DecalNet">
<Guid>{572B87C4-93BD-46B3-A291-CD58181D25DC}</Guid>
<VersionMajor>2</VersionMajor>
<VersionMinor>0</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>primary</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>True</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\packages\Fody.6.8.0\build\Fody.targets" Condition="Exists('..\packages\Fody.6.8.0\build\Fody.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this machine. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\Fody.6.8.0\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Fody.6.8.0\build\Fody.targets'))" />
<Error Condition="!Exists('..\packages\Costura.Fody.5.7.0\build\Costura.Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Costura.Fody.5.7.0\build\Costura.Fody.targets'))" />
</Target>
<Import Project="..\packages\Costura.Fody.5.7.0\build\Costura.Fody.targets" Condition="Exists('..\packages\Costura.Fody.5.7.0\build\Costura.Fody.targets')" />
</Project>

View file

@ -0,0 +1,336 @@
using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using Mag.Shared;
using Decal.Adapter;
using Decal.Adapter.Wrappers;
using System.Diagnostics;
using YamlDotNet.Serialization;
namespace MosswartMassacre
{
class MossyInventory : IDisposable
{
private string InventoryFileName
{
get
{
// 1) Character name
var characterName = CoreManager.Current.CharacterFilter.Name;
// 2) Plugin folder - handle hot reload scenarios
string pluginFolder;
if (!string.IsNullOrEmpty(PluginCore.AssemblyDirectory))
{
pluginFolder = PluginCore.AssemblyDirectory;
}
else
{
pluginFolder = Path.GetDirectoryName(
System.Reflection.Assembly
.GetExecutingAssembly()
.Location
);
}
// 3) Character-specific folder path
var characterFolder = Path.Combine(pluginFolder, characterName);
// 4) Ensure directory exists (can do it here, thread-safe for most single-user plugin cases)
if (!Directory.Exists(characterFolder))
Directory.CreateDirectory(characterFolder);
// 5) Return full path to the .json file inside the character folder
return Path.Combine(characterFolder, $"{characterName}.json");
}
}
public MossyInventory()
{
try
{
CoreManager.Current.CharacterFilter.LoginComplete += CharacterFilter_LoginComplete;
CoreManager.Current.WorldFilter.CreateObject += WorldFilter_CreateObject;
CoreManager.Current.WorldFilter.ChangeObject += WorldFilter_ChangeObject;
CoreManager.Current.CharacterFilter.Logoff += CharacterFilter_Logoff;
PluginCore.WriteToChat($"[INV] {InventoryFileName}");
PluginCore.WriteToChat("Started MOSSY!");
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[INV] {ex}");
}
}
private bool disposed;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposed) return;
if (disposing)
{
CoreManager.Current.CharacterFilter.LoginComplete -= CharacterFilter_LoginComplete;
CoreManager.Current.WorldFilter.CreateObject -= WorldFilter_CreateObject;
CoreManager.Current.WorldFilter.ChangeObject -= WorldFilter_ChangeObject;
CoreManager.Current.CharacterFilter.Logoff -= CharacterFilter_Logoff;
}
disposed = true;
}
private bool loginComplete;
private bool loggedInAndWaitingForIdData;
private readonly List<int> requestedIds = new List<int>();
private void CharacterFilter_LoginComplete(object sender, EventArgs e)
{
try
{
loginComplete = true;
// Defensive check - settings might not be initialized yet due to event handler order
bool inventoryLogEnabled;
try
{
inventoryLogEnabled = PluginSettings.Instance.InventoryLog;
}
catch (InvalidOperationException)
{
PluginCore.WriteToChat("[INV] Settings not ready, skipping inventory check");
return;
}
if (!inventoryLogEnabled)
return;
if (!File.Exists(InventoryFileName))
{
PluginCore.WriteToChat("Requesting id information for all armor/weapon inventory. This will take a few minutes...");
foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
{
if (!wo.HasIdData && ObjectClassNeedsIdent(wo.ObjectClass, wo.Name))
CoreManager.Current.Actions.RequestId(wo.Id);
}
loggedInAndWaitingForIdData = true;
}
else
{
DumpInventoryToFile(true);
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[INV] {ex}");
}
}
private void WorldFilter_CreateObject(object sender, CreateObjectEventArgs e)
{
if (!loginComplete) return;
try
{
if (!PluginSettings.Instance.InventoryLog) return;
}
catch (InvalidOperationException)
{
return; // Settings not ready, skip silently
}
if (!e.New.HasIdData && ObjectClassNeedsIdent(e.New.ObjectClass, e.New.Name)
&& !requestedIds.Contains(e.New.Id)
&& e.New.Container == CoreManager.Current.CharacterFilter.Id)
{
requestedIds.Add(e.New.Id);
CoreManager.Current.Actions.RequestId(e.New.Id);
}
}
private void WorldFilter_ChangeObject(object sender, ChangeObjectEventArgs e)
{
if (!loginComplete) return;
try
{
if (!PluginSettings.Instance.InventoryLog) return;
}
catch (InvalidOperationException)
{
return; // Settings not ready, skip silently
}
if (loggedInAndWaitingForIdData)
{
bool allHaveId = true;
foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
{
if (!wo.HasIdData && ObjectClassNeedsIdent(wo.ObjectClass, wo.Name))
{
allHaveId = false;
break;
}
}
if (allHaveId)
{
loggedInAndWaitingForIdData = false;
DumpInventoryToFile();
PluginCore.WriteToChat("Requesting id information for all armor/weapon inventory completed. Log file written.");
}
}
else
{
if (!e.Changed.HasIdData && ObjectClassNeedsIdent(e.Changed.ObjectClass, e.Changed.Name)
&& !requestedIds.Contains(e.Changed.Id)
&& e.Changed.Container == CoreManager.Current.CharacterFilter.Id)
{
requestedIds.Add(e.Changed.Id);
CoreManager.Current.Actions.RequestId(e.Changed.Id);
}
}
}
private void CharacterFilter_Logoff(object sender, Decal.Adapter.Wrappers.LogoffEventArgs e)
{
try
{
try
{
if (!PluginSettings.Instance.InventoryLog) return;
}
catch (InvalidOperationException)
{
return; // Settings not ready, skip silently
}
DumpInventoryToFile(true); // Request IDs if missing to ensure complete data
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[INV] {ex}");
}
}
private void DumpInventoryToFile(bool requestIdsIfMissing = false)
{
var previouslySaved = new List<MyWorldObject>();
if (File.Exists(InventoryFileName))
{
try
{
string oldJson = File.ReadAllText(InventoryFileName);
previouslySaved = JsonConvert.DeserializeObject<List<MyWorldObject>>(oldJson)
?? new List<MyWorldObject>();
}
catch (Exception)
{
PluginCore.WriteToChat("Inventory file is corrupt.");
}
}
var currentList = new List<MyWorldObject>();
foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
{
// Check to see if we already have some information for this item
foreach (var prev in previouslySaved)
{
if (prev.Id == wo.Id && prev.ObjectClass == (int)wo.ObjectClass)
{
// If neither our past nor our current item HadIdData, but it should, lets request it
if (requestIdsIfMissing && !prev.HasIdData && !wo.HasIdData && ObjectClassNeedsIdent(wo.ObjectClass, wo.Name))
{
CoreManager.Current.Actions.RequestId(wo.Id);
currentList.Add(MyWorldObjectCreator.Create(wo));
}
else
{
// Add the WorldObject to the MyWorldObject data so we have up to date information
currentList.Add(MyWorldObjectCreator.Combine(prev, wo));
}
goto end;
}
}
if (requestIdsIfMissing && !wo.HasIdData && ObjectClassNeedsIdent(wo.ObjectClass, wo.Name))
CoreManager.Current.Actions.RequestId(wo.Id);
currentList.Add(MyWorldObjectCreator.Create(wo));
end: ;
}
var fi = new FileInfo(InventoryFileName);
if (fi.Directory != null && !fi.Directory.Exists)
fi.Directory.Create();
string json = JsonConvert.SerializeObject(currentList, Formatting.Indented);
File.WriteAllText(InventoryFileName, json);
// Send full inventory via WebSocket
if (PluginCore.WebSocketEnabled)
{
_ = WebSocket.SendFullInventoryAsync(currentList);
PluginCore.WriteToChat("Inventory sent to MosswartOverlord");
}
}
private bool ObjectClassNeedsIdent(ObjectClass oc, string name)
{
return oc == ObjectClass.Armor
|| oc == ObjectClass.Clothing
|| oc == ObjectClass.MeleeWeapon
|| oc == ObjectClass.MissileWeapon
|| oc == ObjectClass.WandStaffOrb
|| oc == ObjectClass.Jewelry
|| (oc == ObjectClass.Gem && !string.IsNullOrEmpty(name) && name.Contains("Aetheria"))
|| (oc == ObjectClass.Misc && !string.IsNullOrEmpty(name) && name.Contains("Essence"));
}
/// <summary>
/// Forces an inventory upload with ID requests - guarantees complete data
/// </summary>
public void ForceInventoryUpload()
{
try
{
// Check if inventory logging is enabled
try
{
if (!PluginSettings.Instance.InventoryLog)
{
PluginCore.WriteToChat("[INV] Inventory logging is disabled");
return;
}
}
catch (InvalidOperationException)
{
PluginCore.WriteToChat("[INV] Settings not ready");
return;
}
// Check if WebSocket is enabled
if (!PluginCore.WebSocketEnabled)
{
PluginCore.WriteToChat("[INV] WebSocket streaming is disabled");
return;
}
PluginCore.WriteToChat("[INV] Forcing inventory upload with ID requests...");
DumpInventoryToFile(true); // Request IDs if missing
PluginCore.WriteToChat("[INV] Inventory upload completed");
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[INV] Force upload failed: {ex.Message}");
}
}
}
}

View file

@ -0,0 +1,412 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using Decal.Adapter;
using Decal.Adapter.Wrappers;
namespace MosswartMassacre
{
public class NavWaypoint
{
public double NS { get; set; }
public double EW { get; set; }
public double Z { get; set; }
public int Type { get; set; }
public NavWaypoint Previous { get; set; }
}
public class NavRoute : IDisposable
{
private bool disposed = false;
private List<NavWaypoint> waypoints = new List<NavWaypoint>();
private List<D3DObj> lineObjects = new List<D3DObj>();
private Color routeColor;
private bool isVisible = false;
public string FilePath { get; private set; }
public string FileName => Path.GetFileNameWithoutExtension(FilePath);
public bool IsVisible => isVisible;
public int WaypointCount => waypoints.Count;
public NavRoute(string filePath, Color color)
{
FilePath = filePath;
routeColor = color;
}
public bool LoadFromFile()
{
try
{
ClearRoute();
waypoints.Clear();
if (!File.Exists(FilePath))
{
PluginCore.WriteToChat($"Nav file not found: {FilePath}");
return false;
}
PluginCore.WriteToChat($"Navigation: Loading {FileName}...");
using (StreamReader sr = File.OpenText(FilePath))
{
// Read header
string header = sr.ReadLine();
if (string.IsNullOrEmpty(header) || !header.StartsWith("uTank2 NAV"))
{
PluginCore.WriteToChat($"Navigation: Invalid file format - {FileName}");
return false;
}
// Read nav type
string navTypeLine = sr.ReadLine();
if (string.IsNullOrEmpty(navTypeLine) || !int.TryParse(navTypeLine.Trim(), out int navType))
{
PluginCore.WriteToChat($"Navigation: Failed to parse route type - {FileName}");
return false;
}
string navTypeDescription = "";
switch (navType)
{
case 0:
navTypeDescription = "Linear";
break;
case 1:
navTypeDescription = "Circular";
break;
case 2:
navTypeDescription = "Linear";
break;
case 3:
navTypeDescription = "Target (follow player/object)";
break;
case 4:
navTypeDescription = "Once";
break;
default:
navTypeDescription = $"Unknown ({navType})";
PluginCore.WriteToChat($"Navigation: Unknown route type {navType} in {FileName}");
break;
}
// Handle target nav (type 3) - follows a specific player/object
if (navType == 3)
{
if (sr.EndOfStream)
{
PluginCore.WriteToChat($"Navigation: Target route file is empty - {FileName}");
return false;
}
string targetName = sr.ReadLine();
if (sr.EndOfStream)
{
PluginCore.WriteToChat($"Navigation: Target route missing target ID - {FileName}");
return false;
}
string targetIdLine = sr.ReadLine();
PluginCore.WriteToChat($"Navigation: Target route '{targetName}' cannot be visualized");
return true; // Successfully loaded but can't visualize
}
// Read record count
string recordCountLine = sr.ReadLine();
if (string.IsNullOrEmpty(recordCountLine) || !int.TryParse(recordCountLine.Trim(), out int recordCount))
{
PluginCore.WriteToChat($"Navigation: Failed to parse waypoint count - {FileName}");
return false;
}
if (recordCount <= 0 || recordCount > 10000) // Sanity check
{
PluginCore.WriteToChat($"Navigation: Invalid waypoint count {recordCount} - {FileName}");
return false;
}
NavWaypoint previous = null;
int waypointsRead = 0;
while (!sr.EndOfStream && waypointsRead < recordCount)
{
// Read waypoint type
string waypointTypeLine = sr.ReadLine();
if (string.IsNullOrEmpty(waypointTypeLine) || !int.TryParse(waypointTypeLine.Trim(), out int waypointType))
{
PluginCore.WriteToChat($"Navigation: Failed to parse waypoint {waypointsRead + 1} in {FileName}");
break; // Skip this waypoint, don't fail entirely
}
// Read coordinates (all waypoint types have EW, NS, Z, Unknown)
string ewLine = sr.ReadLine();
string nsLine = sr.ReadLine();
string zLine = sr.ReadLine();
string unknownLine = sr.ReadLine(); // Unknown value (always 0)
if (string.IsNullOrEmpty(ewLine) || string.IsNullOrEmpty(nsLine) || string.IsNullOrEmpty(zLine) || string.IsNullOrEmpty(unknownLine))
{
PluginCore.WriteToChat($"Navigation: Missing coordinates at waypoint {waypointsRead + 1} in {FileName}");
break;
}
if (!double.TryParse(ewLine.Trim(), out double ew) ||
!double.TryParse(nsLine.Trim(), out double ns) ||
!double.TryParse(zLine.Trim(), out double z))
{
PluginCore.WriteToChat($"Navigation: Invalid coordinates at waypoint {waypointsRead + 1} in {FileName}");
break; // Skip this waypoint
}
var waypoint = new NavWaypoint
{
NS = ns,
EW = ew,
Z = z,
Type = waypointType,
Previous = previous
};
waypoints.Add(waypoint);
previous = waypoint;
waypointsRead++;
// Skip additional data based on waypoint type
if (!SkipWaypointData(sr, waypointType))
{
PluginCore.WriteToChat($"Navigation: Failed to parse waypoint {waypointsRead + 1} data in {FileName}");
break; // Don't continue if we can't parse properly
}
}
if (waypoints.Count > 0)
{
PluginCore.WriteToChat($"Navigation: Loaded {FileName} ({waypoints.Count} waypoints)");
}
else
{
PluginCore.WriteToChat($"Navigation: No valid waypoints found in {FileName}");
}
return waypoints.Count > 0;
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Navigation: Error loading {FileName} - {ex.Message}");
return false;
}
}
private bool SkipWaypointData(StreamReader sr, int waypointType)
{
try
{
// Skip additional lines based on waypoint type (base 4 lines already read)
switch (waypointType)
{
case 0: // Point - no additional data (4 lines total)
break;
case 1: // Portal - 5 additional lines (9 lines total)
sr.ReadLine(); // Name
sr.ReadLine(); // ObjectClass
sr.ReadLine(); // "true"
sr.ReadLine(); // PortalNS
sr.ReadLine(); // PortalEW
sr.ReadLine(); // PortalZ
break;
case 2: // Recall - 1 additional line (5 lines total)
sr.ReadLine(); // RecallSpellId
break;
case 3: // Pause - 1 additional line (5 lines total)
sr.ReadLine(); // Pause milliseconds
break;
case 4: // ChatCommand - 1 additional line (5 lines total)
sr.ReadLine(); // Message
break;
case 5: // OpenVendor - 2 additional lines (6 lines total)
sr.ReadLine(); // Id
sr.ReadLine(); // Name
break;
case 6: // Portal2 - same as Portal (9 lines total)
sr.ReadLine(); // Name
sr.ReadLine(); // ObjectClass
sr.ReadLine(); // "true"
sr.ReadLine(); // PortalNS
sr.ReadLine(); // PortalEW
sr.ReadLine(); // PortalZ
break;
case 7: // UseNPC - 5 additional lines (9 lines total)
sr.ReadLine(); // Name
sr.ReadLine(); // ObjectClass
sr.ReadLine(); // "true"
sr.ReadLine(); // NpcEW
sr.ReadLine(); // NpcNS
sr.ReadLine(); // NpcZ
break;
case 8: // Checkpoint - no additional data (4 lines total)
break;
case 9: // Jump - 3 additional lines (7 lines total)
sr.ReadLine(); // Heading
sr.ReadLine(); // ShiftJump
sr.ReadLine(); // Milliseconds
break;
default:
// Unknown waypoint type - skip silently
break;
}
return true;
}
catch
{
// Silently handle parsing errors
return false;
}
}
public void Show()
{
if (isVisible) return;
if (waypoints.Count == 0)
{
PluginCore.WriteToChat($"Navigation: No waypoints to visualize in {FileName}");
return;
}
CreateLineObjects();
isVisible = true;
PluginCore.WriteToChat($"Navigation: Showing {FileName} ({waypoints.Count} waypoints)");
}
public void Hide()
{
if (!isVisible) return;
ClearRoute();
isVisible = false;
PluginCore.WriteToChat($"Navigation: Hidden {FileName}");
}
private void CreateLineObjects()
{
try
{
// Check D3DService availability
if (CoreManager.Current?.D3DService == 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
int linesCreated = 0;
for (int i = 1; i <= maxLines; i++)
{
var current = waypoints[i];
var previous = waypoints[i - 1];
if (CreateLineBetweenWaypoints(previous, current))
{
linesCreated++;
}
// Add small delay every 50 lines to prevent UI freezing
if (i % 50 == 0)
{
System.Threading.Thread.Sleep(1);
}
}
if (waypoints.Count > 501)
{
PluginCore.WriteToChat($"Navigation: Large route - showing {maxLines} of {waypoints.Count} segments");
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Navigation: Error creating visualization - {ex.Message}");
}
}
private bool CreateLineBetweenWaypoints(NavWaypoint from, NavWaypoint to)
{
try
{
// Calculate distance
double distance = Math.Sqrt(
Math.Pow((to.NS - from.NS) * 240, 2) +
Math.Pow((to.EW - from.EW) * 240, 2) +
Math.Pow((to.Z - from.Z) * 240, 2)
);
if (distance <= 0) return false;
// Create D3D line object
var lineObj = CoreManager.Current.D3DService.NewD3DObj();
if (lineObj == null) return false;
lineObj.SetShape(D3DShape.Cube);
lineObj.Color = 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
lineObj.Anchor(midNS, midEW, midZ);
// Orient toward destination
float orientNS = (float)from.NS;
float orientEW = (float)from.EW;
float orientZ = (float)(from.Z * 240) + 0.1f;
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;
lineObj.Visible = true;
lineObjects.Add(lineObj);
return true;
}
catch
{
return false;
}
}
private void ClearRoute()
{
foreach (var obj in lineObjects)
{
try
{
obj.Visible = false;
obj.Dispose();
}
catch { }
}
lineObjects.Clear();
}
public void Dispose()
{
if (!disposed)
{
ClearRoute();
waypoints.Clear();
disposed = true;
}
}
}
}

View file

@ -0,0 +1,246 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using Decal.Adapter;
using Microsoft.Win32;
namespace MosswartMassacre
{
public class NavVisualization : IDisposable
{
private bool disposed = false;
private NavRoute currentRoute = null;
private string vtankProfilesDirectory = "";
private List<string> availableNavFiles = new List<string>();
// Default comparison route color (red)
private readonly Color comparisonRouteColor = Color.FromArgb(255, 255, 100, 100);
public bool IsEnabled { get; private set; } = false;
public bool HasRouteLoaded => currentRoute != null && currentRoute.WaypointCount > 0;
public string CurrentRouteFile => currentRoute?.FileName ?? "None";
public List<string> AvailableNavFiles => availableNavFiles.ToList();
public event EventHandler RouteChanged;
public NavVisualization()
{
InitializeVTankDirectory();
RefreshNavFileList();
}
private void InitializeVTankDirectory()
{
try
{
// First, check if user has configured a custom path
if (!string.IsNullOrEmpty(PluginSettings.Instance?.VTankProfilesPath))
{
vtankProfilesDirectory = PluginSettings.Instance.VTankProfilesPath;
return;
}
// Try to get VTank directory from Windows Registry (same method as UtilityBelt)
var defaultPath = @"C:\Games\VirindiPlugins\VirindiTank\";
try
{
var regKey = Registry.LocalMachine.OpenSubKey("Software\\Decal\\Plugins\\{642F1F48-16BE-48BF-B1D4-286652C4533E}");
if (regKey != null)
{
var profilePath = regKey.GetValue("ProfilePath")?.ToString();
if (!string.IsNullOrEmpty(profilePath))
{
vtankProfilesDirectory = profilePath;
return;
}
}
}
catch
{
}
// Fall back to default path
vtankProfilesDirectory = defaultPath;
// Using default path - user can configure in Settings if needed
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[NavViz] Error finding VTank directory: {ex.Message}");
vtankProfilesDirectory = "";
}
}
/// <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
InitializeVTankDirectory();
availableNavFiles.Clear();
if (string.IsNullOrEmpty(vtankProfilesDirectory))
{
PluginCore.WriteToChat("VTank directory not configured. Set path in Settings tab.");
return;
}
if (!Directory.Exists(vtankProfilesDirectory))
{
PluginCore.WriteToChat($"VTank directory not found: {vtankProfilesDirectory}");
return;
}
try
{
// Get all files and filter for .nav files only, excluding follow/temporary files
var allFiles = Directory.GetFiles(vtankProfilesDirectory);
var navFiles = allFiles
.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))
.OrderBy(name => name)
.ToList();
availableNavFiles.AddRange(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($"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
{
// Clear current route
if (currentRoute != null)
{
currentRoute.Dispose();
currentRoute = null;
}
if (string.IsNullOrEmpty(navFileName) || navFileName == "None")
{
RouteChanged?.Invoke(this, EventArgs.Empty);
return true;
}
string fullPath = Path.Combine(vtankProfilesDirectory, navFileName + ".nav");
if (!File.Exists(fullPath))
{
PluginCore.WriteToChat($"Navigation file '{navFileName}' not found");
return false;
}
currentRoute = new NavRoute(fullPath, comparisonRouteColor);
if (!currentRoute.LoadFromFile())
{
currentRoute.Dispose();
currentRoute = null;
return false;
}
// Show route if visualization is enabled
if (IsEnabled)
{
currentRoute.Show();
}
RouteChanged?.Invoke(this, EventArgs.Empty);
return true;
}
catch (Exception ex)
{
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)
{
// No change needed if already in desired state
if (IsEnabled == enabled) return;
IsEnabled = enabled;
if (currentRoute != null)
{
if (enabled)
{
currentRoute.Show();
}
else
{
currentRoute.Hide();
}
}
else
{
}
PluginCore.WriteToChat($"Navigation visualization {(enabled ? "enabled" : "disabled")}");
}
public void ToggleEnabled()
{
SetEnabled(!IsEnabled);
}
public string GetStatus()
{
if (currentRoute == null)
return "No route loaded";
string status = $"{currentRoute.FileName} ({currentRoute.WaypointCount} points)";
if (IsEnabled && currentRoute.IsVisible)
status += " - Visible";
else if (IsEnabled)
status += " - Hidden";
else
status += " - Disabled";
return status;
}
public void Dispose()
{
if (!disposed)
{
if (currentRoute != null)
{
currentRoute.Dispose();
currentRoute = null;
}
disposed = true;
}
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -17,17 +17,53 @@ namespace MosswartMassacre
private bool _rareMetaEnabled = true;
private bool _httpServerEnabled = false;
private bool _telemetryEnabled = false;
private bool _webSocketEnabled = false;
private bool _inventorylog = true;
private string _charTag = "default";
private int _mainWindowX = 100;
private int _mainWindowY = 100;
private bool _useTabbedInterface = true;
private string _vtankProfilesPath = "";
private bool _verboseLogging = false;
public static PluginSettings Instance => _instance
?? throw new InvalidOperationException("PluginSettings not initialized");
public static void Initialize()
{
// determine settings file path
// determine plugin folder and character-specific folder
string characterName = CoreManager.Current.CharacterFilter.Name;
string pluginFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
_filePath = Path.Combine(pluginFolder, $"{characterName}.yaml");
// For hot reload scenarios, use the AssemblyDirectory set by the Loader
// For normal loading, fall back to the executing assembly location
string pluginFolder;
if (!string.IsNullOrEmpty(PluginCore.AssemblyDirectory))
{
pluginFolder = PluginCore.AssemblyDirectory;
}
else
{
pluginFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
}
// Path for character-specific folder
string characterFolder = Path.Combine(pluginFolder, characterName);
// Create the character folder if it doesn't exist
if (!Directory.Exists(characterFolder))
{
try
{
Directory.CreateDirectory(characterFolder);
}
catch (Exception ex)
{
PluginCore.DispatchChatToBoxWithPluginIntercept($"[Settings] Failed to create character folder: {ex.Message}");
}
}
// YAML file is now in the character-specific folder
_filePath = Path.Combine(characterFolder, $"{characterName}.yaml");
// build serializer/deserializer once
var builder = new DeserializerBuilder()
@ -123,11 +159,50 @@ namespace MosswartMassacre
get => _telemetryEnabled;
set { _telemetryEnabled = value; Save(); }
}
public bool WebSocketEnabled
{
get => _webSocketEnabled;
set { _webSocketEnabled = value; Save(); }
}
public string CharTag
{
get => _charTag;
set { _charTag = value; Save(); }
}
public bool InventoryLog
{
get => _inventorylog;
set { _inventorylog = value; Save(); }
}
public int MainWindowX
{
get => _mainWindowX;
set { _mainWindowX = value; Save(); }
}
public int MainWindowY
{
get => _mainWindowY;
set { _mainWindowY = value; Save(); }
}
public bool UseTabbedInterface
{
get => _useTabbedInterface;
set { _useTabbedInterface = value; Save(); }
}
public string VTankProfilesPath
{
get => _vtankProfilesPath;
set { _vtankProfilesPath = value; Save(); }
}
public bool VerboseLogging
{
get => _verboseLogging;
set { _verboseLogging = value; Save(); }
}
}
}

View file

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

View file

@ -0,0 +1,313 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Decal.Adapter;
using Decal.Adapter.Wrappers;
namespace MosswartMassacre
{
/// <summary>
/// Quest tracking and management system
/// Ported from UBS Lua quest system
/// </summary>
public class QuestManager : IDisposable
{
#region Quest Data Structures
public class Quest
{
public string Id { get; set; }
public int Solves { get; set; }
public int Timestamp { get; set; }
public string Description { get; set; }
public int MaxSolves { get; set; }
public int Delta { get; set; }
public int ExpireTime { get; set; }
}
#endregion
#region Properties
public List<Quest> QuestList { get; private set; }
public Dictionary<string, Quest> QuestDictionary { get; private set; }
#endregion
#region Events and State
private bool isRefreshing = false;
private DateTime lastRefreshTime = DateTime.MinValue;
#endregion
public QuestManager()
{
QuestList = new List<Quest>();
QuestDictionary = new Dictionary<string, Quest>();
// Hook into chat events for quest parsing
InitializeChatHooks();
}
#region Initialization
private void InitializeChatHooks()
{
try
{
if (CoreManager.Current != null)
{
CoreManager.Current.ChatBoxMessage += OnChatBoxMessage;
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error initializing quest chat hooks: {ex.Message}");
}
}
#endregion
#region Quest Name Mapping
public string GetFriendlyQuestName(string questStamp)
{
return QuestNames.GetFriendlyName(questStamp);
}
public string GetQuestDisplayName(string questStamp)
{
return QuestNames.GetDisplayName(questStamp);
}
public int GetQuestNameMappingsCount()
{
return QuestNames.QuestStampToName.Count;
}
#endregion
#region Quest Parsing
private void OnChatBoxMessage(object sender, ChatTextInterceptEventArgs e)
{
try
{
if (!isRefreshing || string.IsNullOrEmpty(e.Text))
return;
// Parse quest information from /myquests output
ParseQuestLine(e.Text);
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error parsing quest line: {ex.Message}");
}
}
private void ParseQuestLine(string text)
{
try
{
// Quest line format: TaskName - Solves solves (Timestamp)"Description" MaxSolves Delta
// Example: "SomeQuest - 5 solves (1640995200)"Quest description here" 10 3600
var pattern = @"([^\-]+) - (\d+) solves \((\d+)\)""([^""]+)"" (-?\d+) (\d+)";
var match = Regex.Match(text, pattern);
if (match.Success)
{
var quest = new Quest
{
Id = match.Groups[1].Value.Trim(),
Solves = int.Parse(match.Groups[2].Value),
Timestamp = int.Parse(match.Groups[3].Value),
Description = match.Groups[4].Value,
MaxSolves = int.Parse(match.Groups[5].Value),
Delta = int.Parse(match.Groups[6].Value)
};
quest.ExpireTime = quest.Timestamp + quest.Delta;
// Add to collections
QuestList.Add(quest);
QuestDictionary[quest.Id] = quest;
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error parsing quest line '{text}': {ex.Message}");
}
}
#endregion
#region Quest Management
public void RefreshQuests()
{
try
{
if (isRefreshing)
return;
ClearQuests();
isRefreshing = true;
// Issue /myquests command to refresh quest data
CoreManager.Current.Actions.InvokeChatParser("/myquests");
// Stop listening after a delay
System.Threading.Timer stopTimer = null;
stopTimer = new System.Threading.Timer(_ =>
{
isRefreshing = false;
stopTimer?.Dispose();
lastRefreshTime = DateTime.Now;
}, null, 3000, System.Threading.Timeout.Infinite);
}
catch (Exception ex)
{
isRefreshing = false;
PluginCore.WriteToChat($"Error refreshing quests: {ex.Message}");
}
}
public void ClearQuests()
{
QuestList.Clear();
QuestDictionary.Clear();
}
public bool IsQuestAvailable(string questStamp)
{
if (!QuestDictionary.TryGetValue(questStamp, out Quest quest))
return true; // If quest not found, assume available
var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
return quest.ExpireTime < currentTime;
}
public bool IsQuestMaxSolved(string questStamp)
{
if (!QuestDictionary.TryGetValue(questStamp, out Quest quest))
return false;
return quest.Solves >= quest.MaxSolves;
}
public bool HasQuestFlag(string questStamp)
{
return QuestDictionary.ContainsKey(questStamp);
}
public string GetTimeUntilExpire(Quest quest)
{
if (quest == null)
return "Unknown";
var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var timeLeft = quest.ExpireTime - currentTime;
if (timeLeft <= 0)
return "Ready";
return FormatSeconds((int)timeLeft);
}
public string FormatTimeStamp(int timestamp)
{
try
{
var dateTime = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime;
return dateTime.ToString("MM/dd/yyyy HH:mm:ss");
}
catch
{
return "Invalid";
}
}
public string FormatSeconds(int seconds)
{
if (seconds <= 0)
return "0s";
var days = seconds / 86400;
seconds %= 86400;
var hours = seconds / 3600;
seconds %= 3600;
var minutes = seconds / 60;
seconds %= 60;
var result = "";
if (days > 0) result += $"{days}d ";
if (hours > 0) result += $"{hours}h ";
if (minutes > 0) result += $"{minutes}m ";
if (seconds > 0 || string.IsNullOrEmpty(result)) result += $"{seconds}s";
return result.Trim();
}
public object GetFieldByID(Quest quest, int id)
{
if (quest == null)
return null;
switch (id)
{
case 1: return quest.Id;
case 2: return quest.Solves;
case 3: return quest.Timestamp;
case 4: return quest.MaxSolves;
case 5: return quest.Delta;
case 6: return quest.ExpireTime;
default: return quest.Id;
}
}
#endregion
#region Society Quest Helpers
public string GetSocietyName(int factionBits)
{
switch (factionBits)
{
case 1: return "Celestial Hand";
case 2: return "Eldrytch Web";
case 4: return "Radiant Blood";
default: return "Unknown";
}
}
public string GetSocietyRank(int ribbons)
{
if (ribbons >= 1001) return "Master";
if (ribbons >= 601) return "Lord";
if (ribbons >= 301) return "Knight";
if (ribbons >= 101) return "Adept";
if (ribbons >= 1) return "Initiate";
return "None";
}
public int GetMaxRibbonsPerDay(string rank)
{
switch (rank)
{
case "Initiate": return 50;
case "Adept": return 100;
case "Knight": return 150;
case "Lord": return 200;
case "Master": return 250;
default: return 0;
}
}
#endregion
#region Cleanup
public void Dispose()
{
try
{
if (CoreManager.Current != null)
{
CoreManager.Current.ChatBoxMessage -= OnChatBoxMessage;
}
ClearQuests();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error disposing quest manager: {ex.Message}");
}
}
#endregion
}
}

View file

@ -0,0 +1,228 @@
using System.Collections.Generic;
namespace MosswartMassacre
{
/// <summary>
/// Static quest name mappings from quest stamp to friendly display name
/// Based on questtracker repository data
/// </summary>
public static class QuestNames
{
/// <summary>
/// Dictionary mapping quest stamps to friendly quest names
/// </summary>
public static readonly Dictionary<string, string> QuestStampToName = new Dictionary<string, string>
{
// Character-specific Quest Stamps (from actual /myquests output)
["30minattributes"] = "30 Minute Attribute Gems Timer",
["academeyexittokengiven"] = "Academy Exit Token Received",
["aerbaxchestkey2pickup"] = "Aerbax Chest Key #2 Pickup",
["anekshaygemofknowledgetimer_monthly"] = "A'nekshay Gem of Knowledge Monthly Timer",
["anekshaygemoflesserknowledgecollectedinamonth"] = "A'nekshay Gems of Lesser Knowledge Monthly Count",
["anekshaygemoflesserknowledgetimer_monthly"] = "A'nekshay Gem of Lesser Knowledge Monthly Timer",
["attributereset30day"] = "30-Day Attribute Reset Timer",
["augmentationblankgemacquired"] = "Blank Augmentation Gem Pickup Timer",
["bellowsnewbieturnedin"] = "Blacksmith's Bellows Turned In",
["bonecrunchkeypickuptimer"] = "Bonecrunch's Key Pickup Timer",
["callingstonegiven"] = "Calling Stone Turned Over",
["defeatedbonecrunch"] = "Bonecrunch Defeated",
["efmlcentermanafieldused"] = "EF Middle Level Center Mana Field Used",
["efmleastmanafieldused"] = "EF Middle Level East Mana Field Used",
["efmlnorthmanafieldused"] = "EF Middle Level North Mana Field Used",
["efmlsouthmanafieldused"] = "EF Middle Level South Mana Field Used",
["efmlwestmanafieldused"] = "EF Middle Level West Mana Field Used",
["efulcentermanafieldused"] = "EF Upper Level Center Mana Field Used",
["efuleastmanafieldused"] = "EF Upper Level East Mana Field Used",
["efulnorthmanafieldused"] = "EF Upper Level North Mana Field Used",
["efulsouthmanafieldused"] = "EF Upper Level South Mana Field Used",
["efulwestmanafieldused"] = "EF Upper Level West Mana Field Used",
["insatiableeaterjaw"] = "Insatiable Eater Jaw Collection",
["pathwardencomplete"] = "Pathwarden Visit Complete",
["pathwardenfound1111"] = "Pathwarden Greeter Encountered",
["recallsingularitycaul"] = "Recall Singularity Bore Pickup",
["stipendscollectedinamonth"] = "Monthly Stipends Collected Count",
["stipendtimer_0812"] = "Stipend Collection Timer",
["stipendtimer_monthly"] = "Monthly Stipend Timer",
["upperinsatiablejaw"] = "Upper Insatiable Eater Jaw Collection",
["usedattributereset"] = "Attribute Reset Used",
["usedfreeattributereset"] = "Free Attribute Reset Used",
["usedfreeskillreset"] = "Free Skill Reset Used",
["usedskillreset"] = "Skill Reset Used",
["virindiisland"] = "Singularity Island Visit",
// Kill Tasks
["turshscalp"] = "Tursh Scalp",
["polarursuin"] = "Polar Ursuin Kill Task Main Flag Timer",
["polarursuinkillcount"] = "Polar Ursuin Kill Counter",
["polardillotask"] = "Polar Dillo Kill Task Main Flag",
["polardillokills"] = "Polar Dillo Kill Counter",
["repugnanteaterkilltask"] = "Repugnant Eater Kill Task",
["repugeaterkillcount"] = "Repugnant Eater Kill Counter",
["deathcap"] = "Deathcap Thrungus Kill Task",
["deathcapkillcount"] = "Deathcap Thrungus Kill Counter",
["grievverv"] = "Grievver Violator Kill Task",
["grievvervkillcount"] = "Grievver Violator Kill Counter",
["tuskerg"] = "Tusker Guard Kill Task Main Flag",
["tuskergkillcount"] = "Tusker Guard Kill Counter",
// Quest Timers and Pickups
["blankaug"] = "Blank Aug Gem Pickup Timer",
["greatcavepenguinegg"] = "Great Cave Penguin Egg Pickup Timer",
["deathallurecd"] = "Death's Allure Timer Flag",
["brewmastercover"] = "Brew Master Quest Pickup Timer Cover",
["brewmasterback"] = "Brew Master Quest Pickup Timer Back",
["brewmasterpages"] = "Brew Master Quest Pickup Timer Pages",
["brewmasterspine"] = "Brew Master Quest Pickup Timer Spine",
["eleonorasheart"] = "Elanora's Heart Quest Pickup Timer",
["beacongemobtained"] = "Cooldown for obtaining another beacon gem",
["beaconcomplete"] = "Beacon Quest Complete Timer",
["sirginaziosword"] = "Pick up of Sir Ginazio's Sword",
// Major Quests
["maraudersjaw"] = "Marauder's Lair Quest",
["fledgemastertusk"] = "Fledge Master's Tusk Quest",
["crystallinekiller"] = "Crystalline Killer",
["darkisledelivery"] = "Dark Isle Delivery",
["defeatingvaeshok"] = "Defeating Vaeshok",
["hollyjollyhelperquest"] = "Holly Jolly Helper Quest",
["moarsmenjailbreak"] = "Moarsmen Jailbreak",
["shamblingarchivistdestroyer"] = "Shambling Archivist Destroyer",
["tracingthestone"] = "Tracing The Stone",
["undeadjawcollection"] = "Undead Jaw Collection",
["weedingofthederutree"] = "Weeding of the Deru Tree",
["ironbladecommander"] = "Iron Blade Commander",
["mumiyahhuntingneftet"] = "Mumiyah Hunting Neftet",
["torgashstasks"] = "Torgash's Tasks",
// Thrungus Hovels Items
["stolenfryingpan"] = "Thrungus Hovels",
["stolenring"] = "Thrungus Hovels",
["stolenbrewkettle"] = "Thrungus Hovels",
["stolenamulet"] = "Thrungus Hovels",
["stolenewer"] = "Thrungus Hovels",
["stolennecklace"] = "Thrungus Hovels",
["stolenplatter"] = "Thrungus Hovels",
["stolenbracelet"] = "Thrungus Hovels",
// Special Items and Flags
["ringofkarlun"] = "Knights of Karlun",
["trainingacademycomplete"] = "Completion of Training Academy for Exit",
["cowtipcounter"] = "Counter for Cow Tipping",
["cowtip"] = "Main Timed Flag for Cow Tipping",
["skillloweringgempickedup"] = "Picked up a forgetfulness gem",
// Healing Machine Components
["orbhealingmachine"] = "Healing Machine Orb",
["pedestalhealingmachine"] = "Healing Machine Pedestal",
["tihnhealingmachine"] = "Healing Machine Tihn",
["lavushealingmachine"] = "Healing Machine Lavus",
["hookhealingmachine"] = "Healing Machine Hook",
// Eater Jaws
["ravenouseaterjaw"] = "Ravenous Eater Jaw",
["insatiableeaterjaw"] = "Insatiable Eater Jaw",
["engorgedeaterjaw"] = "Engorged Eater Jaw",
["voraciouseaterjaw"] = "Voracious Eater Jaw",
["abhorrenteaterjaw"] = "Abhorrent Eater Jaw",
// Kill Tasks (Extended)
["altereddrudgekilltask"] = "Altered Drudge Kill Task",
["altereddrudgekillcount"] = "Altered Drudge Kill Counter",
["arcticmattekarkilltask"] = "Arctic Mattekar Kill Task",
["arcticmattekarkillcount"] = "Arctic Mattekar Kill Counter",
["armoredillohuntingneftetkilltask"] = "Armoredillo Hunting Neftet Kill Task",
["armoredillohuntingneftetkillcount"] = "Armoredillo Hunting Neftet Kill Counter",
["augmenteddrudgekilltask"] = "Augmented Drudge Kill Task",
["augmenteddrudgekillcount"] = "Augmented Drudge Kill Counter",
["banishedcreaturekilltask"] = "Banished Creature Kill Task",
["banishedcreaturekillcount"] = "Banished Creature Kill Counter",
["benekniffiskilltask"] = "Benek Niffis Kill Task",
["benekniffiskillcount"] = "Benek Niffis Kill Counter",
["blackcoralgolemkilltask"] = "Black Coral Golem Kill Task",
["blackcoralgolemkillcount"] = "Black Coral Golem Kill Counter",
["blessedmoarsmankilltask"] = "Blessed Moarsman Kill Task",
["blessedmoarsmankillcount"] = "Blessed Moarsman Kill Counter",
["blightedcoralgolemkilltask"] = "Blighted Coral Golem Kill Task",
["blightedcoralgolemkillcount"] = "Blighted Coral Golem Kill Counter",
["bloodshrethkilltask"] = "Blood Shreth Kill Task",
["bloodshrethkillcount"] = "Blood Shreth Kill Counter",
["bronzegauntlettrooperkilltask"] = "Bronze Gauntlet Trooper Kill Task",
["bronzegauntlettrooperkillcount"] = "Bronze Gauntlet Trooper Kill Counter",
["coppercogtrooperkilltask"] = "Copper Cog Trooper Kill Task",
["coppercogtrooperkillcount"] = "Copper Cog Trooper Kill Counter",
["coppergolemkingpinkilltask"] = "Copper Golem Kingpin Kill Task",
["coppergolemkingpinkillcount"] = "Copper Golem Kingpin Kill Counter",
["coralgolemkilltask"] = "Coral Golem Kill Task",
["coralgolemkillcount"] = "Coral Golem Kill Counter",
["coralgolemviceroykilltask"] = "Coral Golem Viceroy Kill Task",
["coralgolemviceroykillcount"] = "Coral Golem Viceroy Kill Counter",
["corruptedgravestonekilltask"] = "Corrupted Gravestone Kill Task",
["corruptedgravestonekillcount"] = "Corrupted Gravestone Kill Counter",
["deathcapthrunguskilltask"] = "Deathcap Thrungus Kill Task",
["deathcapthrunguskillcount"] = "Deathcap Thrungus Kill Counter",
["desertcactuskilltask"] = "Desert Cactus Kill Task",
["desertcactuskillcount"] = "Desert Cactus Kill Counter",
["devourermargulkilltask"] = "Devourer Margul Kill Task",
["devourermargulkillcount"] = "Devourer Margul Kill Counter",
// Society and Faction Quests
["celestialhandintroductioncomplete"] = "Celestial Hand Introduction Complete",
["eldrytchwebintroductioncomplete"] = "Eldrytch Web Introduction Complete",
["radiantbloodintroductioncomplete"] = "Radiant Blood Introduction Complete",
["celestialhandinitiatetest"] = "Celestial Hand Initiate Test",
["eldrytchwebinitiatetest"] = "Eldrytch Web Initiate Test",
["radiantbloodinitiatetest"] = "Radiant Blood Initiate Test",
// Luminance Aura Related
["aetheriaredemption"] = "Aetheria Redemption",
["aegisofmerc"] = "Aegis of Merc",
["lumaugtradein"] = "Luminance Augmentation Trade In",
// Common AC Quests
["holtburgtraderskill"] = "Holtburg Trader Skill Quest",
["shoushitraderskill"] = "Shoushi Trader Skill Quest",
["yaraqtraderskill"] = "Yaraq Trader Skill Quest",
["newbiequests"] = "Newbie Academy Quests",
["moarsmanraid"] = "Moarsman Raid",
["virindiparadox"] = "Virindi Paradox",
["portalspace"] = "Portal Space Exploration"
};
/// <summary>
/// Get friendly name for a quest stamp, with fallback to original stamp
/// </summary>
/// <param name="questStamp">The quest stamp to lookup</param>
/// <returns>Friendly name if found, otherwise the original quest stamp</returns>
public static string GetFriendlyName(string questStamp)
{
if (string.IsNullOrEmpty(questStamp))
return questStamp;
return QuestStampToName.TryGetValue(questStamp.ToLower(), out string friendlyName)
? friendlyName
: questStamp;
}
/// <summary>
/// Get display name with friendly name and original stamp in parentheses
/// </summary>
/// <param name="questStamp">The quest stamp to format</param>
/// <returns>Formatted display name</returns>
public static string GetDisplayName(string questStamp)
{
if (string.IsNullOrEmpty(questStamp))
return questStamp;
string friendlyName = GetFriendlyName(questStamp);
// If we found a mapping, show friendly name with original in parentheses
if (!string.Equals(friendlyName, questStamp, System.StringComparison.OrdinalIgnoreCase))
{
return $"{friendlyName} ({questStamp})";
}
// Otherwise just show the original
return questStamp;
}
}
}

View file

@ -0,0 +1,227 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Mag.Shared.Spells;
namespace MosswartMassacre
{
/// <summary>
/// Manages spell identification and cantrip detection for the Flag Tracker
/// </summary>
public static class SpellManager
{
private static readonly Dictionary<int, Spell> SpellsById = new Dictionary<int, Spell>();
private static readonly List<string[]> SpellData = new List<string[]>();
private static bool isInitialized = false;
static SpellManager()
{
Initialize();
}
private static void Initialize()
{
if (isInitialized) return;
try
{
// Load spell data from embedded CSV resource
var assembly = Assembly.GetExecutingAssembly();
// Try to find the resource with different naming patterns
var availableResources = assembly.GetManifestResourceNames();
var spellResource = availableResources.FirstOrDefault(r => r.Contains("Spells.csv"));
if (string.IsNullOrEmpty(spellResource))
{
// If not embedded, try to load from file system
var csvPath = Path.Combine(Path.GetDirectoryName(assembly.Location), "..", "Shared", "Spells", "Spells.csv");
if (File.Exists(csvPath))
{
LoadFromFile(csvPath);
isInitialized = true;
return;
}
}
else
{
using (var stream = assembly.GetManifestResourceStream(spellResource))
{
if (stream != null)
{
using (var reader = new StreamReader(stream))
{
LoadFromReader(reader);
}
}
}
}
isInitialized = true;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"SpellManager initialization error: {ex.Message}");
}
}
private static void LoadFromFile(string path)
{
using (var reader = new StreamReader(path))
{
LoadFromReader(reader);
}
}
private static void LoadFromReader(StreamReader reader)
{
// Skip header line
var header = reader.ReadLine();
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
if (!string.IsNullOrWhiteSpace(line))
{
var parts = line.Split(',');
if (parts.Length >= 6) // Minimum required fields
{
SpellData.Add(parts);
// Parse spell data
if (int.TryParse(parts[0], out int id))
{
var name = parts[1];
int.TryParse(parts[3], out int difficulty);
int.TryParse(parts[4], out int duration);
int.TryParse(parts[5], out int family);
var spell = new Spell(id, name, difficulty, duration, family);
SpellsById[id] = spell;
}
}
}
}
}
/// <summary>
/// Gets a spell by its ID
/// </summary>
public static Spell GetSpell(int id)
{
if (SpellsById.TryGetValue(id, out var spell))
return spell;
return null;
}
/// <summary>
/// Gets a spell by its name (case-insensitive)
/// </summary>
public static Spell GetSpell(string name)
{
return SpellsById.Values.FirstOrDefault(s =>
string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Gets the total number of spells loaded
/// </summary>
public static int GetSpellCount()
{
return SpellsById.Count;
}
/// <summary>
/// Detects if a spell is a cantrip and returns its info
/// </summary>
public static CantripInfo DetectCantrip(Spell spell)
{
if (spell == null || spell.CantripLevel == Spell.CantripLevels.None)
return null;
var info = new CantripInfo
{
SpellId = spell.Id,
Name = spell.Name,
Level = GetCantripLevelName(spell.CantripLevel),
Color = GetCantripColor(spell.CantripLevel)
};
// Extract skill/attribute name from spell name
info.SkillName = ExtractSkillFromSpellName(spell.Name, info.Level);
return info;
}
private static string GetCantripLevelName(Spell.CantripLevels level)
{
switch (level)
{
case Spell.CantripLevels.Minor: return "Minor";
case Spell.CantripLevels.Moderate: return "Moderate";
case Spell.CantripLevels.Major: return "Major";
case Spell.CantripLevels.Epic: return "Epic";
case Spell.CantripLevels.Legendary: return "Legendary";
default: return "N/A";
}
}
private static System.Drawing.Color GetCantripColor(Spell.CantripLevels level)
{
switch (level)
{
case Spell.CantripLevels.Minor: return System.Drawing.Color.White;
case Spell.CantripLevels.Moderate: return System.Drawing.Color.Green;
case Spell.CantripLevels.Major: return System.Drawing.Color.Blue;
case Spell.CantripLevels.Epic: return System.Drawing.Color.Purple;
case Spell.CantripLevels.Legendary: return System.Drawing.Color.Orange;
default: return System.Drawing.Color.White;
}
}
private static string ExtractSkillFromSpellName(string spellName, string level)
{
// Remove the cantrip level prefix
var skillPart = spellName;
if (!string.IsNullOrEmpty(level) && skillPart.StartsWith(level + " "))
{
skillPart = skillPart.Substring(level.Length + 1);
}
// Map common spell name patterns to skill names
if (skillPart.Contains("Strength")) return "Strength";
if (skillPart.Contains("Endurance")) return "Endurance";
if (skillPart.Contains("Coordination")) return "Coordination";
if (skillPart.Contains("Quickness")) return "Quickness";
if (skillPart.Contains("Focus")) return "Focus";
if (skillPart.Contains("Self") || skillPart.Contains("Willpower")) return "Willpower";
// Protection mappings
if (skillPart.Contains("Armor")) return "Armor";
if (skillPart.Contains("Bludgeoning")) return "Bludgeoning Ward";
if (skillPart.Contains("Piercing")) return "Piercing Ward";
if (skillPart.Contains("Slashing")) return "Slashing Ward";
if (skillPart.Contains("Flame") || skillPart.Contains("Fire")) return "Flame Ward";
if (skillPart.Contains("Frost") || skillPart.Contains("Cold")) return "Frost Ward";
if (skillPart.Contains("Acid")) return "Acid Ward";
if (skillPart.Contains("Lightning") || skillPart.Contains("Electric")) return "Storm Ward";
// Return the skill part as-is if no mapping found
return skillPart;
}
/// <summary>
/// Information about a detected cantrip
/// </summary>
public class CantripInfo
{
public int SpellId { get; set; }
public string Name { get; set; }
public string SkillName { get; set; }
public string Level { get; set; }
public System.Drawing.Color Color { get; set; }
}
}
}

View file

@ -85,9 +85,10 @@ namespace MosswartMassacre
kills = PluginCore.totalKills,
onlinetime = (DateTime.Now - PluginCore.statsStartTime).ToString(@"dd\.hh\:mm\:ss"),
kills_per_hour = PluginCore.killsPerHour.ToString("F0"),
deaths = 0,
deaths = PluginCore.sessionDeaths.ToString(),
total_deaths = PluginCore.totalDeaths.ToString(),
rares_found = PluginCore.rareCount,
prismatic_taper_count = 0,
prismatic_taper_count = PluginCore.cachedPrismaticCount.ToString(),
vt_state = VtankControl.VtGetMetaState(),
};

View file

@ -0,0 +1,233 @@
using System;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Threading.Tasks;
namespace MosswartMassacre
{
public static class UpdateManager
{
private const string UPDATE_URL = "https://git.snakedesert.se/SawatoMosswartsEnjoyersClub/MosswartMassacre/raw/branch/spawn-detection/MosswartMassacre/bin/Release/MosswartMassacre.dll";
private static bool updateAvailable = false;
private static string remoteFileHash = string.Empty;
private static string localFileHash = string.Empty;
private static DateTime lastCheckTime = DateTime.MinValue;
public static bool IsUpdateAvailable => updateAvailable;
public static DateTime LastCheckTime => lastCheckTime;
/// <summary>
/// Calculate SHA256 hash of a file
/// </summary>
private static string CalculateFileHash(string filePath)
{
using (var sha256 = SHA256.Create())
{
using (var stream = File.OpenRead(filePath))
{
byte[] hashBytes = sha256.ComputeHash(stream);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
}
}
/// <summary>
/// Calculate SHA256 hash of byte array
/// </summary>
private static string CalculateHash(byte[] data)
{
using (var sha256 = SHA256.Create())
{
byte[] hashBytes = sha256.ComputeHash(data);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
}
public static async Task<bool> CheckForUpdateAsync()
{
try
{
PluginCore.WriteToChat("[Update] Checking for updates...");
// Get local file hash
string localPath = GetLocalDllPath();
if (!File.Exists(localPath))
{
PluginCore.WriteToChat("[Update] Error: Could not find local DLL file");
return false;
}
PluginCore.WriteToChat("[Update] Calculating local file hash...");
localFileHash = CalculateFileHash(localPath);
// Download remote file and calculate hash
using (var client = new HttpClient())
{
client.Timeout = TimeSpan.FromSeconds(30);
PluginCore.WriteToChat("[Update] Downloading remote file for comparison...");
var remoteData = await client.GetByteArrayAsync(UPDATE_URL);
if (remoteData == null || remoteData.Length == 0)
{
PluginCore.WriteToChat("[Update] Error: Could not download remote file");
return false;
}
PluginCore.WriteToChat("[Update] Calculating remote file hash...");
remoteFileHash = CalculateHash(remoteData);
}
// Compare hashes
updateAvailable = !string.Equals(localFileHash, remoteFileHash, StringComparison.OrdinalIgnoreCase);
lastCheckTime = DateTime.Now;
if (updateAvailable)
{
PluginCore.WriteToChat($"[Update] Update available!");
PluginCore.WriteToChat($"[Update] Local hash: {localFileHash}");
PluginCore.WriteToChat($"[Update] Remote hash: {remoteFileHash}");
}
else
{
PluginCore.WriteToChat("[Update] Up to date - hashes match");
}
return true;
}
catch (HttpRequestException ex)
{
PluginCore.WriteToChat($"[Update] Network error: {ex.Message}");
return false;
}
catch (TaskCanceledException)
{
PluginCore.WriteToChat("[Update] Request timed out");
return false;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[Update] Check failed: {ex.Message}");
return false;
}
}
public static async Task<bool> DownloadAndInstallUpdateAsync()
{
if (!updateAvailable)
{
PluginCore.WriteToChat("[Update] No update available. Run /mm checkforupdate first.");
return false;
}
try
{
PluginCore.WriteToChat("[Update] Downloading update...");
string localPath = GetLocalDllPath();
string tempPath = localPath + ".tmp";
string backupPath = localPath + ".bak";
// Download to temp file
using (var client = new HttpClient())
{
client.Timeout = TimeSpan.FromSeconds(30);
var response = await client.GetAsync(UPDATE_URL);
response.EnsureSuccessStatusCode();
using (var fileStream = File.Create(tempPath))
{
await response.Content.CopyToAsync(fileStream);
}
}
// Validate downloaded file by hash
PluginCore.WriteToChat("[Update] Validating downloaded file...");
var downloadedHash = CalculateFileHash(tempPath);
if (!string.Equals(downloadedHash, remoteFileHash, StringComparison.OrdinalIgnoreCase))
{
File.Delete(tempPath);
PluginCore.WriteToChat($"[Update] Download validation failed. Hash mismatch!");
PluginCore.WriteToChat($"[Update] Expected: {remoteFileHash}");
PluginCore.WriteToChat($"[Update] Got: {downloadedHash}");
return false;
}
PluginCore.WriteToChat("[Update] Download complete, installing...");
// Atomically replace current file with new version (creates backup automatically)
File.Replace(tempPath, localPath, backupPath);
// Clear update flag
updateAvailable = false;
PluginCore.WriteToChat("[Update] Update installed successfully!");
PluginCore.WriteToChat("[Update] Previous version backed up as MosswartMassacre.dll.bak");
// Wait a moment for file system to settle, then trigger hot reload
await System.Threading.Tasks.Task.Delay(1000);
try
{
// Touch the file to ensure FileSystemWatcher detects the change
File.SetLastWriteTime(localPath, DateTime.Now);
PluginCore.WriteToChat("[Update] Triggering hot reload...");
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[Update] Could not trigger hot reload: {ex.Message}");
PluginCore.WriteToChat("[Update] Please use /mm gui to reload manually");
}
return true;
}
catch (HttpRequestException ex)
{
PluginCore.WriteToChat($"[Update] Download error: {ex.Message}");
return false;
}
catch (TaskCanceledException)
{
PluginCore.WriteToChat("[Update] Download timed out");
return false;
}
catch (UnauthorizedAccessException)
{
PluginCore.WriteToChat("[Update] File access denied. Make sure the plugin directory is writable.");
return false;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[Update] Install failed: {ex.Message}");
return false;
}
}
private static string GetLocalDllPath()
{
// Get the path to the current DLL
string assemblyPath = typeof(PluginCore).Assembly.Location;
// If empty (hot reload scenario), use AssemblyDirectory + filename
if (string.IsNullOrEmpty(assemblyPath))
{
return Path.Combine(PluginCore.AssemblyDirectory, "MosswartMassacre.dll");
}
return assemblyPath;
}
public static string GetUpdateStatus()
{
if (lastCheckTime == DateTime.MinValue)
{
return "Update Status: Not checked";
}
return updateAvailable ? "Update Status: Update available" : "Update Status: Up to date";
}
}
}

View file

@ -2,6 +2,8 @@
using Decal.Adapter;
using Decal.Adapter.Wrappers;
using System.Numerics;
using Mag.Shared.Constants;
using System.Linq;
namespace MosswartMassacre
{
@ -39,6 +41,29 @@ namespace MosswartMassacre
}
}
/// <summary>
/// Return any WorldObject's raw world position by reading the
/// physics-object pointer (same offsets: +0x84/88/8C).
/// </summary>
public static unsafe Vector3 GetWorldObjectPosition(int objectId)
{
try
{
if (!CoreManager.Current.Actions.IsValidObject(objectId))
return new Vector3();
byte* p = (byte*)CoreManager.Current.Actions.Underlying.GetPhysicsObjectPtr(objectId);
return new Vector3(
*(float*)(p + 0x84), // X
*(float*)(p + 0x88), // Y
*(float*)(p + 0x8C)); // Z
}
catch
{
return new Vector3();
}
}
/// <summary>
/// Convenience: returns the current landcell (upper 16 bits of landblock).
/// </summary>
@ -63,6 +88,26 @@ namespace MosswartMassacre
return new Coordinates(ew, ns, pos.Z);
}
/// <summary>
/// Get AC-style coordinates (EW/NS/Z) for any WorldObject.
/// </summary>
public static Coordinates GetWorldObjectCoordinates(WorldObject wo)
{
if (wo == null) return new Coordinates();
Vector3 pos = GetWorldObjectPosition(wo.Id);
// Get landcell from the object's coordinates
var coordsObj = wo.Coordinates();
if (coordsObj == null) return new Coordinates();
// Convert DECAL coords to our Coordinates with Z
double ew = coordsObj.EastWest;
double ns = coordsObj.NorthSouth;
return new Coordinates(ew, ns, pos.Z);
}
/* ----------------------------------------------------------
* 3) Generic math helpers you may want later
* -------------------------------------------------------- */
@ -72,5 +117,108 @@ namespace MosswartMassacre
public static double DegToRad(double deg) => deg * Math.PI / 180.0;
public static double RadToDeg(double rad) => rad * 180.0 / Math.PI;
/* ----------------------------------------------------------
* 4) Generic item property access
* -------------------------------------------------------- */
/// <summary>
/// Find a WorldObject item by name in inventory
/// </summary>
/// <param name="itemName">Name of the item to find</param>
/// <returns>WorldObject or null if not found</returns>
public static WorldObject FindItemByName(string itemName)
{
try
{
//var worldFilter = CoreManager.Current.WorldFilter;
//var playerInv = CoreManager.Current.CharacterFilter.Id;
// Search inventory
foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
{
if (string.Equals(wo.Name, itemName, StringComparison.OrdinalIgnoreCase))
return wo;
}
return null;
}
catch
{
return null;
}
}
/// <summary>
/// Get the stack size/quantity of a specific item by name
/// </summary>
/// <param name="itemName">Name of the item to find</param>
/// <returns>Stack size or 0 if not found</returns>
/// <summary>
/// Return the total quantity of an item in the characters inventory,
/// adding up every stack that shares <paramref name="itemName"/>.
/// </summary>
public static int GetItemStackSize(string itemName)
{
try
{
// 1. Pull every WorldObject in bags + containers
var inv = CoreManager.Current.WorldFilter.GetInventory();
// 2. Keep only those whose display name matches (case-insensitive)
// 3. For each one, use StackCount if it exists, otherwise treat as 1
return inv.Where(wo =>
string.Equals(wo.Name, itemName,
StringComparison.OrdinalIgnoreCase))
.Sum(wo =>
{
// Some items (weapons, armor) arent stackable;
// Values(LongValueKey.StackCount) throws if the key is absent.
try
{
return wo.Values(LongValueKey.StackCount);
}
catch
{
return 1; // non-stackable item = quantity 1
}
});
}
catch
{
return 0;
}
}
/// <summary>
/// Get the icon ID of a specific item by name
/// </summary>
/// <param name="itemName">Name of the item to find</param>
/// <returns>Icon ID or 0 if not found</returns>
public static int GetItemIcon(string itemName)
{
try
{
var item = FindItemByName(itemName);
return item?.Icon ?? 0;
}
catch
{
return 0;
}
}
/// <summary>
/// Get the display icon ID (with 0x6000000 offset) for an item by name
/// </summary>
/// <param name="itemName">Name of the item to find</param>
/// <returns>Display icon ID or 0x6002D14 (default icon) if not found</returns>
public static int GetItemDisplayIcon(string itemName)
{
int rawIcon = GetItemIcon(itemName);
return rawIcon != 0 ? rawIcon + 0x6000000 : 0x6002D14;
}
}
}

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

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<view icon="7735" title="Mossy Tracker v4.0.0.5" width="800" height="600">
<control progid="DecalControls.Notebook" name="mainTabView">
<!-- Augmentations Tab -->
<page label="Augs">
<control progid="DecalControls.FixedLayout" clipped="">
<control progid="DecalControls.PushButton" name="btnRefreshAugs" left="10" top="10" width="100" height="24" text="Refresh"/>
<control progid="DecalControls.List" name="lstAugmentations" left="10" top="40" width="760" height="520">
<column progid="DecalControls.TextColumn" fixedwidth="200" name="Augmentation" />
<column progid="DecalControls.TextColumn" fixedwidth="100" name="Progress" />
<column progid="DecalControls.TextColumn" fixedwidth="200" name="Trainer" />
<column progid="DecalControls.TextColumn" fixedwidth="260" name="Location" />
</control>
</control>
</page>
<!-- Luminance Auras Tab -->
<page label="Lum">
<control progid="DecalControls.FixedLayout" clipped="">
<control progid="DecalControls.PushButton" name="btnRefreshLum" left="10" top="10" width="100" height="24" text="Refresh"/>
<control progid="DecalControls.List" name="lstLuminanceAuras" left="10" top="40" width="760" height="520">
<column progid="DecalControls.TextColumn" fixedwidth="300" name="Luminance Aura" />
<column progid="DecalControls.TextColumn" fixedwidth="150" name="Progress" />
<column progid="DecalControls.TextColumn" fixedwidth="310" name="Category" />
</control>
</control>
</page>
<!-- Recall Spells Tab -->
<page label="Recalls">
<control progid="DecalControls.FixedLayout" clipped="">
<control progid="DecalControls.PushButton" name="btnRefreshRecalls" left="10" top="10" width="100" height="24" text="Refresh"/>
<control progid="DecalControls.List" name="lstRecallSpells" left="10" top="40" width="760" height="520">
<column progid="DecalControls.IconColumn" fixedwidth="20" name="Icon" />
<column progid="DecalControls.TextColumn" fixedwidth="500" name="Recall Spell" />
<column progid="DecalControls.TextColumn" fixedwidth="240" name="Status" />
</control>
</control>
</page>
<!-- Cantrips Tab -->
<page label="Cantrips">
<control progid="DecalControls.FixedLayout" clipped="">
<control progid="DecalControls.PushButton" name="btnRefreshCantrips" left="10" top="10" width="100" height="24" text="Refresh"/>
<control progid="DecalControls.List" name="lstCantrips" left="10" top="40" width="760" height="520">
<column progid="DecalControls.IconColumn" fixedwidth="20" name="Icon" />
<column progid="DecalControls.TextColumn" fixedwidth="520" name="Skill/Effect" />
<column progid="DecalControls.TextColumn" fixedwidth="220" name="Cantrip Level" />
</control>
</control>
</page>
<!-- Quests Tab -->
<page label="Quests">
<control progid="DecalControls.FixedLayout" clipped="">
<control progid="DecalControls.PushButton" name="btnRefreshQuests" left="10" top="10" width="100" height="24" text="Refresh Quests"/>
<control progid="DecalControls.List" name="lstQuests" left="10" top="40" width="760" height="520">
<column progid="DecalControls.TextColumn" fixedwidth="200" name="Quest" />
<column progid="DecalControls.TextColumn" fixedwidth="80" name="Solves" />
<column progid="DecalControls.TextColumn" fixedwidth="100" name="Completed" />
<column progid="DecalControls.TextColumn" fixedwidth="80" name="Max" />
<column progid="DecalControls.TextColumn" fixedwidth="100" name="Delta" />
<column progid="DecalControls.TextColumn" fixedwidth="200" name="Expire" />
</control>
</control>
</page>
</control>
</view>

View file

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="utf-8"?>
<view icon="7735" title="Mosswart Massacre v4.0.0.5" width="420" height="320">
<control progid="DecalControls.Notebook" name="MainNotebook">
<page label="Main">
<control progid="DecalControls.FixedLayout" clipped="">
<!-- Current kill tracking display -->
<control progid="DecalControls.StaticText" name="lblTotalKills" left="10" top="10" width="250" height="20" text="Total Kills: 0"/>
<control progid="DecalControls.StaticText" name="lblKillsPer5Min" left="10" top="30" width="250" height="20" text="Kills per 5 Min: 0"/>
<control progid="DecalControls.StaticText" name="lblKillsPerHour" left="10" top="50" width="250" height="20" text="Kills per Hour: 0"/>
<control progid="DecalControls.StaticText" name="lblElapsedTime" left="10" top="70" width="250" height="20" text="Time: 00:00:00"/>
<control progid="DecalControls.StaticText" name="lblRareCount" left="10" top="90" width="250" height="20" text="Rare Count: 0"/>
<!-- Auto loot rare indicator -->
<control progid="DecalControls.StaticText" name="lblAutoLootRare" left="10" top="120" width="200" height="20" text="Auto Loot Rare: [ON]"/>
<!-- Enhanced status display -->
<control progid="DecalControls.StaticText" name="lblStatus" left="10" top="145" width="380" height="20" text="Status: Ready"/>
<control progid="DecalControls.StaticText" name="lblWebSocketStatus" left="10" top="165" width="380" height="20" text="WebSocket: [DISCONNECTED]"/>
<!-- Update controls on Main tab -->
<control progid="DecalControls.PushButton" name="btnCheckUpdate" left="10" top="190" width="120" height="25" text="Check for Updates"/>
<control progid="DecalControls.PushButton" name="btnInstallUpdate" left="140" top="190" width="120" height="25" text="Install Update"/>
<control progid="DecalControls.StaticText" name="lblUpdateStatus" left="10" top="220" width="250" height="20" text=""/>
</control>
</page>
<page label="Settings">
<control progid="DecalControls.FixedLayout" clipped="">
<!-- Plugin settings -->
<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="Auto rare meta state" checked="true"/>
<!-- Remote commands setting -->
<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="HTTP server (port 8085)" checked="false"/>
<!-- WebSocket setting -->
<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="Telemetry reporting" checked="false"/>
<!-- Character tag setting -->
<control progid="DecalControls.StaticText" name="lblCharTag" left="20" top="165" width="100" height="16" text="Character Tag:"/>
<control progid="DecalControls.Edit" name="txtCharTag" left="125" top="163" width="150" height="20" text="default"/>
<!-- 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."/>
</control>
</page>
<page label="Statistics">
<control progid="DecalControls.FixedLayout" clipped="">
<!-- Enhanced statistics display -->
<control progid="DecalControls.StaticText" name="lblStatsHeader" left="10" top="10" width="200" height="20" text="Detailed Statistics" style="FontBold"/>
<!-- Kill statistics -->
<control progid="DecalControls.StaticText" name="lblDetailedKills" left="10" top="35" width="180" height="16" text="Total Kills This Session:"/>
<control progid="DecalControls.StaticText" name="lblDetailedKillsValue" left="200" top="35" width="100" height="16" text="0"/>
<control progid="DecalControls.StaticText" name="lblBestHour" left="10" top="55" width="180" height="16" text="Best Hour Performance:"/>
<control progid="DecalControls.StaticText" name="lblBestHourValue" left="200" top="55" width="100" height="16" text="0 kills/hr"/>
<control progid="DecalControls.StaticText" name="lblAverageKills" left="10" top="75" width="180" height="16" text="Average Kills/Hour:"/>
<control progid="DecalControls.StaticText" name="lblAverageKillsValue" left="200" top="75" width="100" height="16" text="0 kills/hr"/>
<!-- Rare statistics -->
<control progid="DecalControls.StaticText" name="lblRareStats" left="10" top="105" width="180" height="16" text="Rares Found:"/>
<control progid="DecalControls.StaticText" name="lblRareStatsValue" left="200" top="105" width="100" height="16" text="0"/>
<control progid="DecalControls.StaticText" name="lblRareRate" left="10" top="125" width="180" height="16" text="Rare Drop Rate:"/>
<control progid="DecalControls.StaticText" name="lblRareRateValue" left="200" top="125" width="100" height="16" text="0.00%"/>
<!-- Time statistics -->
<control progid="DecalControls.StaticText" name="lblSessionTime" left="10" top="155" width="180" height="16" text="Session Duration:"/>
<control progid="DecalControls.StaticText" name="lblSessionTimeValue" left="200" top="155" width="100" height="16" text="00:00:00"/>
<control progid="DecalControls.StaticText" name="lblLastKill" left="10" top="175" width="180" height="16" text="Last Kill:"/>
<control progid="DecalControls.StaticText" name="lblLastKillValue" left="200" top="175" width="100" height="16" text="Never"/>
<!-- Reset button -->
<control progid="DecalControls.PushButton" name="btnResetStats" left="10" top="205" width="80" height="24" text="Reset All"/>
</control>
</page>
<page label="Navigation">
<control progid="DecalControls.FixedLayout" clipped="">
<!-- Navigation visualization controls -->
<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="Show route visualization" checked="false"/>
<!-- Nav file selection -->
<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"/>
<!-- Route status -->
<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="Route displays in red"/>
<!-- Instructions -->
<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="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>
<page label="Mossy Tracker">
<control progid="DecalControls.FixedLayout" clipped="">
<!-- Launch button -->
<control progid="DecalControls.PushButton" name="btnOpenFlagTracker" left="10" top="10" width="150" height="30" text="Open Mossy Tracker"/>
</control>
</page>
</control>
</view>

View file

@ -0,0 +1,863 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using VirindiViewService.Controls;
namespace MosswartMassacre.Views
{
/// <summary>
/// Dedicated Flag Tracker window with comprehensive character tracking
/// Ported from UBS Lua flagtracker with full functionality preservation
/// </summary>
internal class FlagTrackerView : VVSBaseView
{
private static FlagTrackerView instance;
#region Tab Control References
private HudTabView mainTabView;
// Augmentations Tab
private HudList lstAugmentations;
private HudButton btnRefreshAugs;
// Luminance Tab
private HudList lstLuminanceAuras;
private HudButton btnRefreshLum;
// Recalls Tab
private HudList lstRecallSpells;
private HudButton btnRefreshRecalls;
// Cantrips Tab
private HudList lstCantrips;
private HudButton btnRefreshCantrips;
// Quests Tab
private HudList lstQuests;
private HudButton btnRefreshQuests;
#endregion
#region Data Management
private FlagTrackerData data;
private System.Timers.Timer questUpdateTimer;
#endregion
public FlagTrackerView(PluginCore core) : base(core)
{
try
{
instance = this;
data = new FlagTrackerData();
// Initialize quest update timer for real-time countdown
questUpdateTimer = new System.Timers.Timer(5000); // Update every 5 seconds
questUpdateTimer.Elapsed += OnQuestTimerUpdate;
questUpdateTimer.AutoReset = true;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[MossyTracker] Failed to initialize: {ex.Message}");
}
}
#region Static Interface
public static void OpenFlagTracker()
{
try
{
if (instance == null)
{
instance = new FlagTrackerView(null);
instance.InitializeView();
}
else
{
// Bring existing window to front
if (instance.view != null)
{
instance.view.Visible = true;
}
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error opening Flag Tracker: {ex.Message}");
}
}
public static void CloseFlagTracker()
{
try
{
if (instance != null)
{
instance.Dispose();
instance = null;
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error closing Flag Tracker: {ex.Message}");
}
}
public static bool IsOpen()
{
return instance != null && instance.view != null && instance.view.Visible;
}
public static void RefreshQuestData()
{
try
{
if (PluginCore.questManager != null)
{
PluginCore.questManager.RefreshQuests();
// If Flag Tracker window is open, also refresh the UI
if (instance != null)
{
instance.PopulateQuestsList();
// Schedule another refresh in a few seconds to catch any data
System.Threading.Timer refreshTimer = null;
refreshTimer = new System.Threading.Timer(_ =>
{
try
{
if (instance != null)
{
instance.PopulateQuestsList();
}
}
catch (Exception timerEx)
{
PluginCore.WriteToChat($"[MossyTracker] Delayed refresh failed: {timerEx.Message}");
}
finally
{
refreshTimer?.Dispose();
}
}, null, 4000, System.Threading.Timeout.Infinite);
}
}
else
{
PluginCore.WriteToChat("[MossyTracker] Quest manager not available for refresh");
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[MossyTracker] Quest refresh failed: {ex.Message}");
}
}
#endregion
#region Initialization
private void InitializeView()
{
try
{
CreateFromXMLResource("MosswartMassacre.ViewXML.flagTracker.xml");
if (view == null)
{
PluginCore.WriteToChat("[MossyTracker] Failed to create view");
return;
}
InitializeTabControls();
InitializeEventHandlers();
Initialize();
if (view != null)
{
view.Visible = true;
view.ShowInBar = true;
view.Title = "Mossy Tracker v5.0.0.0";
}
RefreshAllData();
// Start quest update timer
if (questUpdateTimer != null)
{
questUpdateTimer.Start();
}
PluginCore.WriteToChat("[MossyTracker] Initialized successfully");
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[MossyTracker] Failed to initialize: {ex.Message}");
}
}
private void InitializeTabControls()
{
try
{
mainTabView = GetControl<HudTabView>("mainTabView");
lstAugmentations = GetControl<HudList>("lstAugmentations");
btnRefreshAugs = GetControl<HudButton>("btnRefreshAugs");
lstLuminanceAuras = GetControl<HudList>("lstLuminanceAuras");
btnRefreshLum = GetControl<HudButton>("btnRefreshLum");
lstRecallSpells = GetControl<HudList>("lstRecallSpells");
btnRefreshRecalls = GetControl<HudButton>("btnRefreshRecalls");
lstCantrips = GetControl<HudList>("lstCantrips");
btnRefreshCantrips = GetControl<HudButton>("btnRefreshCantrips");
lstQuests = GetControl<HudList>("lstQuests");
btnRefreshQuests = GetControl<HudButton>("btnRefreshQuests");
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[MossyTracker] Failed to initialize controls: {ex.Message}");
}
}
private void InitializeEventHandlers()
{
try
{
// Refresh button events
if (btnRefreshAugs != null) btnRefreshAugs.Hit += OnRefreshAugmentations;
if (btnRefreshLum != null) btnRefreshLum.Hit += OnRefreshLuminance;
if (btnRefreshRecalls != null) btnRefreshRecalls.Hit += OnRefreshRecalls;
if (btnRefreshCantrips != null) btnRefreshCantrips.Hit += OnRefreshCantrips;
if (btnRefreshQuests != null) btnRefreshQuests.Hit += OnRefreshQuests;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error initializing event handlers: {ex.Message}");
}
}
#endregion
#region Window Management Overrides
protected override void View_VisibleChanged(object sender, EventArgs e)
{
try
{
// Call base implementation first
base.View_VisibleChanged(sender, e);
// If window becomes invisible and we're not already disposed, dispose it
// This handles the X button click case
if (view != null && !view.Visible && !_disposed)
{
// Use a small delay to ensure this isn't just a temporary hide
System.Threading.Timer disposeTimer = null;
disposeTimer = new System.Threading.Timer(_ =>
{
try
{
// Double-check the window is still hidden and not disposed
if (view != null && !view.Visible && !_disposed)
{
Dispose();
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in delayed disposal: {ex.Message}");
}
finally
{
disposeTimer?.Dispose();
}
}, null, 100, System.Threading.Timeout.Infinite); // 100ms delay
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error in FlagTracker VisibleChanged: {ex.Message}");
}
}
#endregion
#region Timer Event Handlers
private void OnQuestTimerUpdate(object sender, System.Timers.ElapsedEventArgs e)
{
try
{
// Update quest list display if quests tab is visible and we have quest data
if (view != null && view.Visible && PluginCore.questManager?.QuestList != null && PluginCore.questManager.QuestList.Count > 0)
{
// Only update the quest list to refresh countdown timers
PopulateQuestsList();
}
}
catch (Exception)
{
// Silently handle timer update errors to avoid spam
}
}
#endregion
#region Event Handlers
private void OnRefreshAugmentations(object sender, EventArgs e)
{
try
{
data.RefreshAugmentations();
PopulateAugmentationsList();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error refreshing augmentations: {ex.Message}");
}
}
private void OnRefreshLuminance(object sender, EventArgs e)
{
try
{
data.RefreshLuminanceAuras();
PopulateLuminanceList();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error refreshing luminance auras: {ex.Message}");
}
}
private void OnRefreshRecalls(object sender, EventArgs e)
{
try
{
data.RefreshRecallSpells();
PopulateRecallsList();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error refreshing recall spells: {ex.Message}");
}
}
private void OnRefreshCantrips(object sender, EventArgs e)
{
try
{
data.RefreshCantrips();
PopulateCantripsList();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error refreshing cantrips: {ex.Message}");
}
}
private void OnRefreshQuests(object sender, EventArgs e)
{
try
{
if (PluginCore.questManager != null)
{
PluginCore.questManager.RefreshQuests();
PopulateQuestsList();
// Schedule another refresh in a few seconds to catch any data
System.Threading.Timer refreshTimer = null;
refreshTimer = new System.Threading.Timer(_ =>
{
try
{
PopulateQuestsList();
}
catch (Exception timerEx)
{
PluginCore.WriteToChat($"[MossyTracker] Refresh failed: {timerEx.Message}");
}
finally
{
refreshTimer?.Dispose();
}
}, null, 4000, System.Threading.Timeout.Infinite);
}
else
{
PluginCore.WriteToChat("[MossyTracker] Quest manager not available");
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[MossyTracker] Refresh failed: {ex.Message}");
}
}
#endregion
#region Helper Methods
private void SafeSetListText(HudList.HudListRowAccessor row, int columnIndex, string text)
{
try
{
if (row != null && columnIndex >= 0)
{
// Check if the column exists
try
{
var control = row[columnIndex];
if (control != null)
{
((HudStaticText)control).Text = text ?? "";
}
// Column control is null - ignore silently
}
catch (IndexOutOfRangeException)
{
// Column doesn't exist - ignore silently
}
}
// Invalid parameters - ignore silently
}
catch
{
// Ignore text setting errors silently
}
}
private void SafeSetListColor(HudList.HudListRowAccessor row, int columnIndex, Color color)
{
try
{
if (row != null && columnIndex >= 0 && row[columnIndex] != null)
{
((HudStaticText)row[columnIndex]).TextColor = color;
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error setting list color at column {columnIndex}: {ex.Message}");
}
}
private void SafeSetListImage(HudList.HudListRowAccessor row, int columnIndex, int iconId)
{
try
{
if (row != null && columnIndex >= 0)
{
try
{
var control = row[columnIndex];
if (control != null && control is VirindiViewService.Controls.HudPictureBox)
{
var pictureBox = (VirindiViewService.Controls.HudPictureBox)control;
pictureBox.Image = iconId;
}
}
catch (IndexOutOfRangeException)
{
// Column doesn't exist - ignore silently
}
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error setting list image at column {columnIndex}: {ex.Message}");
}
}
private string GetIconSymbol(string category)
{
switch (category)
{
case "Basic Recalls":
return "[B]"; // Basic recalls
case "Island Recalls":
return "[I]"; // Island recalls
case "Town Recalls":
return "[T]"; // Town recalls
case "Special Recalls":
return "[S]"; // Special recalls
default:
return "[?]"; // Unknown category
}
}
private string FormatCountdown(long seconds)
{
if (seconds <= 0)
return "READY";
var timeSpan = TimeSpan.FromSeconds(seconds);
if (timeSpan.TotalDays >= 1)
return $"{(int)timeSpan.TotalDays}d {timeSpan.Hours:D2}h";
else if (timeSpan.TotalHours >= 1)
return $"{timeSpan.Hours}h {timeSpan.Minutes:D2}m";
else if (timeSpan.TotalMinutes >= 1)
return $"{timeSpan.Minutes}m {timeSpan.Seconds:D2}s";
else
return $"{timeSpan.Seconds}s";
}
#endregion
#region Data Population Methods
private void RefreshAllData()
{
try
{
if (PluginCore.questManager != null)
{
PluginCore.questManager.RefreshQuests();
}
if (data != null)
{
try
{
data.RefreshAll();
}
catch (Exception dataEx)
{
PluginCore.WriteToChat($"[MossyTracker] Data refresh failed: {dataEx.Message}");
}
}
PopulateAugmentationsList();
PopulateLuminanceList();
PopulateRecallsList();
PopulateCantripsList();
PopulateQuestsList();
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[MossyTracker] Refresh failed: {ex.Message}");
}
}
private void PopulateAugmentationsList()
{
try
{
if (lstAugmentations == null) return;
lstAugmentations.ClearRows();
if (data?.AugmentationCategories == null)
{
var row = lstAugmentations.AddRow();
SafeSetListText(row, 0, "No augmentation data available");
SafeSetListText(row, 1, "Click Refresh to load data");
SafeSetListText(row, 2, "");
SafeSetListText(row, 3, "");
return;
}
foreach (var category in data.AugmentationCategories)
{
// Add category header
var headerRow = lstAugmentations.AddRow();
SafeSetListText(headerRow, 0, $"--- {category.Key} ---");
SafeSetListText(headerRow, 1, "");
SafeSetListText(headerRow, 2, "");
SafeSetListText(headerRow, 3, "");
// Add augmentations in this category
foreach (var aug in category.Value)
{
var row = lstAugmentations.AddRow();
// Augmentation name with progress indicator
string progressText = aug.IsMaxed ? "[MAX]" : $"[{aug.CurrentValue}/{aug.Repeatable}]";
SafeSetListText(row, 0, aug.Name);
SafeSetListText(row, 1, progressText);
SafeSetListText(row, 2, aug.Trainer);
SafeSetListText(row, 3, aug.Location);
// Color code based on completion status
Color progressColor = Color.Red;
if (aug.IsMaxed)
{
progressColor = Color.Green;
}
else if (aug.CurrentValue > 0)
{
progressColor = Color.Yellow;
}
// Apply color to progress text
SafeSetListColor(row, 1, progressColor);
}
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error populating augmentations list: {ex.Message}");
}
}
private void PopulateLuminanceList()
{
try
{
if (lstLuminanceAuras == null || data?.LuminanceAuraCategories == null) return;
lstLuminanceAuras.ClearRows();
foreach (var category in data.LuminanceAuraCategories)
{
// Add category header
var headerRow = lstLuminanceAuras.AddRow();
SafeSetListText(headerRow, 0, $"--- {category.Key} ---");
SafeSetListText(headerRow, 1, "");
SafeSetListText(headerRow, 2, "");
// Add luminance auras in this category
foreach (var aura in category.Value)
{
var row = lstLuminanceAuras.AddRow();
// Aura name
SafeSetListText(row, 0, aura.Name);
// Progress (current/cap)
string progressText = $"{aura.CurrentValue}/{aura.Cap}";
SafeSetListText(row, 1, progressText);
// Category or quest flag for Seer auras
string categoryText = category.Key == "Seer Auras" && !string.IsNullOrEmpty(aura.QuestFlag)
? aura.QuestFlag
: category.Key;
SafeSetListText(row, 2, categoryText);
// Color code based on progress
Color progressColor = Color.Red;
if (aura.CurrentValue >= aura.Cap)
{
progressColor = Color.Green;
}
else if (aura.CurrentValue > 0)
{
progressColor = Color.Yellow;
}
// Apply color to progress text
SafeSetListColor(row, 1, progressColor);
}
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error populating luminance list: {ex.Message}");
}
}
private void PopulateRecallsList()
{
try
{
if (lstRecallSpells == null) return;
lstRecallSpells.ClearRows();
if (data?.RecallSpells == null)
{
var row = lstRecallSpells.AddRow();
SafeSetListImage(row, 0, 0x6002D14); // Default portal icon
SafeSetListText(row, 1, "No recall data available - Click Refresh");
SafeSetListText(row, 2, "Loading...");
return;
}
foreach (var recall in data.RecallSpells)
{
var row = lstRecallSpells.AddRow();
// Column 0: Spell icon using MagTools approach
SafeSetListImage(row, 0, recall.IconId);
// Column 1: Recall spell name
SafeSetListText(row, 1, recall.Name);
// Column 2: Known status
string status = recall.IsKnown ? "Known" : "Unknown";
SafeSetListText(row, 2, status);
// Color code based on known status
Color statusColor = recall.IsKnown ? Color.Green : Color.Red;
SafeSetListColor(row, 2, statusColor);
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error populating recalls list: {ex.Message}");
}
}
private void PopulateCantripsList()
{
try
{
if (lstCantrips == null || data?.Cantrips == null) return;
lstCantrips.ClearRows();
foreach (var category in data.Cantrips)
{
// Add category header
var headerRow = lstCantrips.AddRow();
SafeSetListImage(headerRow, 0, 0x6002856); // Star icon for category headers
SafeSetListText(headerRow, 1, $"--- {category.Key} ---");
SafeSetListText(headerRow, 2, "");
// Add cantrips in this category
foreach (var cantrip in category.Value)
{
var row = lstCantrips.AddRow();
// Column 0: Icon (green/red circle based on status)
SafeSetListImage(row, 0, cantrip.Value.ComputedIconId);
// Column 1: Skill/Attribute name
SafeSetListText(row, 1, cantrip.Key);
// Column 2: Cantrip level
SafeSetListText(row, 2, cantrip.Value.Value);
// Apply color coding based on cantrip level
SafeSetListColor(row, 2, cantrip.Value.Color);
}
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error populating cantrips list: {ex.Message}");
}
}
private void PopulateQuestsList()
{
try
{
if (lstQuests == null) return;
lstQuests.ClearRows();
// Add column headers - New order: Quest Name, Countdown, Last Solved, Cooldown, Solves
var headerRow = lstQuests.AddRow();
SafeSetListText(headerRow, 0, "--- Quest Name ---");
SafeSetListText(headerRow, 1, "Countdown");
SafeSetListText(headerRow, 2, "Last Solved");
SafeSetListText(headerRow, 3, "Cooldown");
SafeSetListText(headerRow, 4, "Solves");
if (PluginCore.questManager?.QuestList != null && PluginCore.questManager.QuestList.Count > 0)
{
var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
// Filter out maxed quests and sort by solve count (highest to lowest)
var visibleQuests = PluginCore.questManager.QuestList
.Where(q => !(q.MaxSolves > 0 && q.Solves >= q.MaxSolves)) // Hide maxed quests
.OrderByDescending(q => q.Solves)
.ThenBy(q => PluginCore.questManager.GetFriendlyQuestName(q.Id));
foreach (var quest in visibleQuests)
{
var questRow = lstQuests.AddRow();
// Column 0: Quest Name (friendly name only, wider)
string questName = PluginCore.questManager.GetFriendlyQuestName(quest.Id);
SafeSetListText(questRow, 0, questName);
// Column 1: Countdown Timer
long timeRemaining = quest.ExpireTime - currentTime;
string countdownText = FormatCountdown(timeRemaining);
SafeSetListText(questRow, 1, countdownText);
// Column 2: Last Solved (date) - moved from Column 3
SafeSetListText(questRow, 2, PluginCore.questManager.FormatTimeStamp(quest.Timestamp));
// Column 3: Cooldown (formatted duration) - moved from Column 4
SafeSetListText(questRow, 3, PluginCore.questManager.FormatSeconds(quest.Delta));
// Column 4: Solves (white text) - moved from Column 5
SafeSetListText(questRow, 4, quest.Solves.ToString());
SafeSetListColor(questRow, 4, System.Drawing.Color.White);
// Color code the countdown based on availability
if (quest.ExpireTime <= currentTime)
{
SafeSetListColor(questRow, 1, System.Drawing.Color.Green); // Ready - green countdown
}
else
{
SafeSetListColor(questRow, 1, System.Drawing.Color.Yellow); // On cooldown - yellow countdown
}
}
}
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error populating quests list: {ex.Message}");
}
}
#endregion
#region Cleanup
private bool _disposed = false;
protected override void Dispose(bool disposing)
{
if (_disposed) return; // Prevent double disposal
if (disposing)
{
try
{
// Clear static instance reference if this is the current instance
if (instance == this)
{
instance = null;
}
// Event handlers will be cleaned up by base class
// Remove button event handlers
if (btnRefreshAugs != null) btnRefreshAugs.Hit -= OnRefreshAugmentations;
if (btnRefreshLum != null) btnRefreshLum.Hit -= OnRefreshLuminance;
if (btnRefreshRecalls != null) btnRefreshRecalls.Hit -= OnRefreshRecalls;
if (btnRefreshCantrips != null) btnRefreshCantrips.Hit -= OnRefreshCantrips;
if (btnRefreshQuests != null) btnRefreshQuests.Hit -= OnRefreshQuests;
if (data != null)
{
data.Dispose();
data = null;
}
// Stop and dispose quest update timer
if (questUpdateTimer != null)
{
questUpdateTimer.Stop();
questUpdateTimer.Elapsed -= OnQuestTimerUpdate;
questUpdateTimer.Dispose();
questUpdateTimer = null;
}
_disposed = true;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"Error disposing Flag Tracker: {ex.Message}");
}
}
base.Dispose(disposing);
}
#endregion
}
}

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 v5.0.0.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 v5.0.0.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
}
}
protected virtual 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
}
}

File diff suppressed because it is too large Load diff

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

@ -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);
}
}
}

View file

@ -104,5 +104,125 @@ namespace MosswartMassacre
{
return vTank.Instance.MacroEnabled;
}
/// <summary>
/// Advances VTank to the next waypoint in the current navigation route.
/// </summary>
/// <returns>
/// 1 if the waypoint was advanced successfully; 0 on failure.
/// </returns>
public static double VtAdvanceWaypoint()
{
try
{
var externalInterface = vTank.Instance;
// Basic validation
if (externalInterface.NavNumPoints == 0)
{
return 0; // No waypoints
}
int currentWaypoint = externalInterface.NavCurrent;
int totalWaypoints = externalInterface.NavNumPoints;
// Check if we can advance
if (currentWaypoint >= totalWaypoints - 1)
{
return 0; // Already at last waypoint
}
// Check navigation type
var navType = (int)externalInterface.NavType;
if (navType == 2 || navType == 4) // Target or Once
{
return 0;
}
// Access the cExternalInterfaceTrustedRelay and get the PC (PluginCore) reference
// From decompiled code: external interface uses PC.NavCurrent which references dz.o.l
var interfaceType = externalInterface.GetType();
// Look for any way to get to the PluginCore instance
// The interface should have access to PC or some way to reach it
// Try to get the underlying assembly and find the PC static field
var assembly = interfaceType.Assembly;
var pluginCoreType = assembly.GetType("uTank2.PluginCore");
if (pluginCoreType != null)
{
// Get the static PC field
var pcField = pluginCoreType.GetField("PC", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
if (pcField != null)
{
var pluginCoreInstance = pcField.GetValue(null);
if (pluginCoreInstance != null)
{
// Try to call the advance method 'i' on the PluginCore instance
// Need to specify parameter types to avoid "Ambiguous match found"
Type[] parameterTypes = new Type[] { typeof(object), assembly.GetType("MetaViewWrappers.MVControlEventArgs") };
var advanceMethod = pluginCoreType.GetMethod("i",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance,
null, parameterTypes, null);
if (advanceMethod != null)
{
// Call with parameters matching: i(object A_0, MVControlEventArgs A_1)
advanceMethod.Invoke(pluginCoreInstance, new object[] { null, null });
return 1;
}
}
}
// Fallback: try to access dz static field directly
var dzField = pluginCoreType.GetField("dz", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
if (dzField != null)
{
var dzObject = dzField.GetValue(null);
if (dzObject != null)
{
// Navigate the dz.o.l path
var dzType = dzObject.GetType();
var oField = dzType.GetField("o", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (oField != null)
{
var oObject = oField.GetValue(dzObject);
if (oObject != null)
{
var oType = oObject.GetType();
var lField = oType.GetField("l", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (lField != null)
{
// Get current value and increment it
int current = (int)lField.GetValue(oObject);
current++;
if (current >= totalWaypoints)
{
current = totalWaypoints - 1;
}
lField.SetValue(oObject, current);
return 1;
}
}
}
}
}
}
return 0;
}
catch (System.Exception ex)
{
PluginCore.WriteToChat("VTank advance error: " + ex.Message);
return 0;
}
}
}
}

View file

@ -0,0 +1,375 @@
// WebSocket.cs
using System;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Decal.Adapter;
using Newtonsoft.Json;
using uTank2;
namespace MosswartMassacre
{
internal static class SessionInfo
{
internal static readonly Guid Guid = Guid.NewGuid();
internal static readonly string GuidString = Guid.ToString("N");
}
// 1) The envelope type for incoming commands
public class CommandEnvelope
{
[JsonProperty("player_name")]
public string PlayerName { get; set; }
[JsonProperty("command")]
public string Command { get; set; }
}
public static class WebSocket
{
// ─── configuration ──────────────────────────
private static readonly Uri WsEndpoint = new Uri("wss://overlord.snakedesert.se/websocket/");
private const string SharedSecret = "your_shared_secret";
private const int IntervalSec = 5;
private static string SessionId = "";
// ─── cached prismatic taper count ─── (now handled by PluginCore event system)
// ─── runtime state ──────────────────────────
private static ClientWebSocket _ws;
private static CancellationTokenSource _cts;
private static bool _enabled;
private static readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
/// <summary>
/// Fires when a valid CommandEnvelope arrives for this character.
/// </summary>
public static event Action<CommandEnvelope> OnServerCommand;
// ─── public API ─────────────────────────────
public static void Start()
{
if (_enabled) return;
_enabled = true;
_cts = new CancellationTokenSource();
PluginCore.WriteToChat("[WebSocket] connecting…");
_ = Task.Run(ConnectAndLoopAsync);
}
public static void Stop()
{
if (!_enabled) return;
_enabled = false;
_cts.Cancel();
_ws?.Abort();
_ws?.Dispose();
_ws = null;
PluginCore.WriteToChat("[WebSocket] DISABLED");
}
// ─── connect / receive / telemetry loop ──────────────────────
private static async Task ConnectAndLoopAsync()
{
while (_enabled && !_cts.IsCancellationRequested)
{
try
{
// 1) Establish connection
_ws = new ClientWebSocket();
_ws.Options.SetRequestHeader("X-Plugin-Secret", SharedSecret);
await _ws.ConnectAsync(WsEndpoint, _cts.Token);
PluginCore.WriteToChat("[WebSocket] CONNECTED");
SessionId = $"{CoreManager.Current.CharacterFilter.Name}-{DateTime.UtcNow:yyyyMMdd-HHmmss}";
// ─── Register this socket under our character name ───
var registerEnvelope = new
{
type = "register",
player_name = CoreManager.Current.CharacterFilter.Name
};
var regJson = JsonConvert.SerializeObject(registerEnvelope);
await SendEncodedAsync(regJson, _cts.Token);
PluginCore.WriteToChat("[WebSocket] REGISTERED");
var buffer = new byte[4096];
// 2) Fire-and-forget receive loop
var receiveTask = Task.Run(async () =>
{
while (_ws.State == WebSocketState.Open && !_cts.Token.IsCancellationRequested)
{
WebSocketReceiveResult result;
try
{
result = await _ws.ReceiveAsync(new ArraySegment<byte>(buffer), _cts.Token);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[WebSocket] receive error: {ex.Message}");
break;
}
if (result.MessageType == WebSocketMessageType.Close)
break;
var msg = Encoding.UTF8.GetString(buffer, 0, result.Count).Trim();
// 3) Parse into CommandEnvelope
CommandEnvelope env;
try
{
env = JsonConvert.DeserializeObject<CommandEnvelope>(msg);
}
catch (JsonException)
{
continue; // skip malformed JSON
}
// 4) Filter by this character name
if (string.Equals(
env.PlayerName,
CoreManager.Current.CharacterFilter.Name,
StringComparison.OrdinalIgnoreCase))
{
// Fire event immediately - let PluginCore handle threading
OnServerCommand?.Invoke(env);
}
}
});
// 5) Inline telemetry loop
PluginCore.WriteToChat("[WebSocket] Starting telemetry loop");
while (_ws.State == WebSocketState.Open && !_cts.Token.IsCancellationRequested)
{
try
{
var json = BuildPayloadJson();
await SendEncodedAsync(json, _cts.Token);
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[WebSocket] Telemetry failed: {ex.Message}");
break; // Exit telemetry loop on failure
}
try
{
await Task.Delay(TimeSpan.FromSeconds(IntervalSec), _cts.Token);
}
catch (OperationCanceledException)
{
PluginCore.WriteToChat("[WebSocket] Telemetry loop cancelled");
break;
}
}
// Log why telemetry loop exited
PluginCore.WriteToChat($"[WebSocket] Telemetry loop ended - State: {_ws?.State}, Cancelled: {_cts.Token.IsCancellationRequested}");
// Wait for receive loop to finish
await receiveTask;
}
catch (OperationCanceledException)
{
PluginCore.WriteToChat("[WebSocket] Connection cancelled");
break;
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[WebSocket] Connection error: {ex.Message}");
}
finally
{
var finalState = _ws?.State.ToString() ?? "null";
PluginCore.WriteToChat($"[WebSocket] Cleaning up connection - Final state: {finalState}");
_ws?.Abort();
_ws?.Dispose();
_ws = null;
}
// Pause before reconnecting
if (_enabled)
{
PluginCore.WriteToChat("[WebSocket] Reconnecting in 2 seconds...");
try { await Task.Delay(2000, CancellationToken.None); } catch { }
}
}
}
// ─── fire-and-forget chat sender ────────────────────
public static async Task SendChatTextAsync(int colorIndex, string chatText)
{
var envelope = new
{
type = "chat",
timestamp = DateTime.UtcNow.ToString("o"),
character_name = CoreManager.Current.CharacterFilter.Name,
text = chatText,
color = colorIndex
};
var json = JsonConvert.SerializeObject(envelope);
await SendEncodedAsync(json, CancellationToken.None);
}
public static async Task SendSpawnAsync(string nsCoord, string ewCoord, string zCoord, string monster)
{
var envelope = new
{
type = "spawn",
timestamp = DateTime.UtcNow.ToString("o"),
character_name = CoreManager.Current.CharacterFilter.Name,
mob = monster,
ns = nsCoord,
ew = ewCoord,
z = zCoord
};
var json = JsonConvert.SerializeObject(envelope);
await SendEncodedAsync(json, CancellationToken.None);
}
public static async Task SendPortalAsync(string nsCoord, string ewCoord, string zCoord, string portalName)
{
var envelope = new
{
type = "portal",
timestamp = DateTime.UtcNow.ToString("o"),
character_name = CoreManager.Current.CharacterFilter.Name,
portal_name = portalName,
ns = nsCoord,
ew = ewCoord,
z = zCoord
};
var json = JsonConvert.SerializeObject(envelope);
await SendEncodedAsync(json, CancellationToken.None);
}
public static async Task SendRareAsync(string rare)
{
var coords = Coordinates.Me;
var envelope = new
{
type = "rare",
timestamp = DateTime.UtcNow.ToString("o"),
character_name = CoreManager.Current.CharacterFilter.Name,
name = rare,
ew = coords.EW,
ns = coords.NS,
z = coords.Z
};
var json = JsonConvert.SerializeObject(envelope);
await SendEncodedAsync(json, CancellationToken.None);
}
public static async Task SendFullInventoryAsync(List<Mag.Shared.MyWorldObject> inventory)
{
var envelope = new
{
type = "full_inventory",
timestamp = DateTime.UtcNow.ToString("o"),
character_name = CoreManager.Current.CharacterFilter.Name,
item_count = inventory.Count,
items = inventory
};
var json = JsonConvert.SerializeObject(envelope);
await SendEncodedAsync(json, CancellationToken.None);
}
public static async Task SendVitalsAsync(object vitalsData)
{
var json = JsonConvert.SerializeObject(vitalsData);
await SendEncodedAsync(json, CancellationToken.None);
}
public static async Task SendQuestDataAsync(string questName, string countdown)
{
var envelope = new
{
type = "quest",
timestamp = DateTime.UtcNow.ToString("o"),
character_name = CoreManager.Current.CharacterFilter.Name,
quest_name = questName,
countdown = countdown
};
var json = JsonConvert.SerializeObject(envelope);
await SendEncodedAsync(json, CancellationToken.None);
}
// ─── shared send helper with locking ───────────────
private static async Task SendEncodedAsync(string text, CancellationToken token)
{
await _sendLock.WaitAsync(token);
try
{
if (_ws == null || _ws.State != WebSocketState.Open)
return;
var bytes = Encoding.UTF8.GetBytes(text);
await _ws.SendAsync(new ArraySegment<byte>(bytes),
WebSocketMessageType.Text,
true,
token);
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[WebSocket] Send error: {ex.Message}");
_ws?.Abort();
_ws?.Dispose();
_ws = null;
}
finally
{
_sendLock.Release();
}
}
// ─── payload builder ──────────────────────────────
// Removed old cache system - now using PluginCore.cachedPrismaticCount
private static string BuildPayloadJson()
{
var tele = new ClientTelemetry();
var coords = Coordinates.Me;
var payload = new
{
type = "telemetry",
character_name = CoreManager.Current.CharacterFilter.Name,
char_tag = PluginCore.CharTag,
session_id = SessionInfo.GuidString,
timestamp = DateTime.UtcNow.ToString("o"),
ew = coords.EW,
ns = coords.NS,
z = coords.Z,
kills = PluginCore.totalKills,
kills_per_hour = PluginCore.killsPerHour.ToString("F0"),
onlinetime = (DateTime.Now - PluginCore.statsStartTime).ToString(@"dd\.hh\:mm\:ss"),
deaths = PluginCore.sessionDeaths.ToString(),
total_deaths = PluginCore.totalDeaths.ToString(),
prismatic_taper_count = PluginCore.cachedPrismaticCount.ToString(),
vt_state = VtankControl.VtGetMetaState(),
mem_mb = tele.MemoryBytes,
cpu_pct = tele.GetCpuUsage(),
mem_handles = tele.HandleCount
};
return JsonConvert.SerializeObject(payload);
}
}
}

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);
}
}
}

View file

@ -14,6 +14,10 @@
<assemblyIdentity name="Decal.Interop.Inject" publicKeyToken="481f17d392f1fb65" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.9.8.3" newVersion="2.9.8.3" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Decal.FileService" publicKeyToken="bd1c8ce002ce221e" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.9.8.3" newVersion="2.9.8.3" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

Binary file not shown.

Binary file not shown.

View file

@ -1,9 +0,0 @@
<?xml version="1.0"?>
<view icon="26075" title="Mosswart Massacre" width="300" height="200">
<control progid="DecalControls.FixedLayout" clipped="">
<control progid="DecalControls.PushButton" name="btnActivate" left="10" top="20" width="100" height="30" text="Activate"/>
<control progid="DecalControls.PushButton" name="btnReport" left="10" top="60" width="100" height="30" text="Report"/>
<control progid="DecalControls.PushButton" name="btnReset" left="10" top="100" width="100" height="30" text="Reset"/>
<control progid="DecalControls.StaticText" name="lblStatus" left="10" top="150" width="280" height="30" text="Mosswart Massacre is ready!"/>
</control>
</view>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Costura.Fody" version="5.7.0" targetFramework="net48" />
<package id="Fody" version="6.8.0" targetFramework="net48" />
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
<package id="YamlDotNet" version="16.3.0" targetFramework="net48" />
</packages>

View file

@ -0,0 +1,234 @@
; Define your application name
!define APPNAME "MosswartMassacre"
!define SOFTWARECOMPANY "MosswartMassacre"
!define APPGUID "{8C97E839-4D05-4A5F-B0C8-E8E778654322}"
!define CLASSNAME "MosswartMassacre.PluginCore"
!define ASSEMBLY "MosswartMassacre.dll"
!define LOADERGUID "{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}"
!define LOADERCLASS "MosswartMassacre.Loader.LoaderCore"
!define LOADERASSEMBLY "MosswartMassacre.Loader.dll"
InstallDir "C:\Games\DecalPlugins\${APPNAME}"
;Icon "Installer\Res\Decal.ico"
!define BUILDPATH ".\..\bin\Release"
!getdllversion "${BUILDPATH}\${ASSEMBLY}" Expv_
!define VERSION ${Expv_1}.${Expv_2}.${Expv_3}
OutFile "${BUILDPATH}\${APPNAME}Installer-${VERSION}.exe"
; Main Install settings
; compressor goes first
SetCompressor LZMA
Name "${APPNAME} ${VERSION}"
InstallDirRegKey HKLM "Software\${SOFTWARECOMPANY}\${APPNAME}" ""
;SetFont "Verdana" 8
; Use compression
; Modern interface settings
!include "MUI.nsh"
!define MUI_ABORTWARNING
!insertmacro MUI_PAGE_WELCOME
;!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
; Set languages (first is default language)
!insertmacro MUI_LANGUAGE "English"
!insertmacro MUI_RESERVEFILE_LANGDLL
; https://nsis.sourceforge.io/Download_and_Install_dotNET_45
Function CheckAndDownloadDotNet48
# Set up our Variables
Var /GLOBAL dotNET48IsThere
Var /GLOBAL dotNET_CMD_LINE
Var /GLOBAL EXIT_CODE
# We are reading a version release DWORD that Microsoft says is the documented
# way to determine if .NET Framework 4.8 is installed
ReadRegDWORD $dotNET48IsThere HKLM "SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" "Release"
IntCmp $dotNET48IsThere 528049 is_equal is_less is_greater
is_equal:
Goto done_compare_not_needed
is_greater:
Goto done_compare_not_needed
is_less:
Goto done_compare_needed
done_compare_needed:
#.NET Framework 4.8 install is *NEEDED*
# Microsoft Download Center EXE:
# Web Bootstrapper: https://go.microsoft.com/fwlink/?LinkId=2085155
# Full Download: https://go.microsoft.com/fwlink/?linkid=2088631
# Setup looks for components\dotNET48Full.exe relative to the install EXE location
# This allows the installer to be placed on a USB stick (for computers without internet connections)
# If the .NET Framework 4.8 installer is *NOT* found, Setup will connect to Microsoft's website
# and download it for you
# Reboot Required with these Exit Codes:
# 1641 or 3010
# Command Line Switches:
# /showrmui /passive /norestart
# Silent Command Line Switches:
# /q /norestart
# Let's see if the user is doing a Silent install or not
IfSilent is_quiet is_not_quiet
is_quiet:
StrCpy $dotNET_CMD_LINE "/q /norestart"
Goto LookForLocalFile
is_not_quiet:
StrCpy $dotNET_CMD_LINE "/showrmui /passive /norestart"
Goto LookForLocalFile
LookForLocalFile:
# Let's see if the user stored the Full Installer
IfFileExists "$EXEPATH\components\dotNET48Full.exe" do_local_install do_network_install
do_local_install:
# .NET Framework found on the local disk. Use this copy
ExecWait '"$EXEPATH\components\dotNET48Full.exe" $dotNET_CMD_LINE' $EXIT_CODE
Goto is_reboot_requested
# Now, let's Download the .NET
do_network_install:
Var /GLOBAL dotNetDidDownload
NSISdl::download "https://go.microsoft.com/fwlink/?linkid=2088631" "$TEMP\dotNET48Web.exe" $dotNetDidDownload
StrCmp $dotNetDidDownload success fail
success:
ExecWait '"$TEMP\dotNET45Web.exe" $dotNET_CMD_LINE' $EXIT_CODE
Goto is_reboot_requested
fail:
MessageBox MB_OK|MB_ICONEXCLAMATION "Unable to download .NET Framework. ${PRODUCT_NAME} will be installed, but will not function without the Framework!"
Goto done_dotNET_function
# $EXIT_CODE contains the return codes. 1641 and 3010 means a Reboot has been requested
is_reboot_requested:
${If} $EXIT_CODE = 1641
${OrIf} $EXIT_CODE = 3010
SetRebootFlag true
${EndIf}
done_compare_not_needed:
# Done dotNET Install
Goto done_dotNET_function
#exit the function
done_dotNET_function:
FunctionEnd
Section "" CoreSection
; Set Section properties
SetOverwrite on
; Set Section Files and Shortcuts
SetOutPath "$INSTDIR\"
File "${BUILDPATH}\${ASSEMBLY}"
File "${BUILDPATH}\${APPNAME}.pdb"
File "${BUILDPATH}\${LOADERASSEMBLY}"
File "${BUILDPATH}\${APPNAME}.Loader.pdb"
; File "${BUILDPATH}\UtilityBelt.Service.Installer.exe"
SectionEnd
Section -FinishSection
WriteRegStr HKLM "Software\${SOFTWARECOMPANY}\${APPNAME}" "" "$INSTDIR"
WriteRegStr HKLM "Software\${SOFTWARECOMPANY}\${APPNAME}" "Version" "${VERSION}"
;Register in decal
ClearErrors
ReadRegStr $0 HKLM "Software\Decal\Plugins\${APPGUID}" ""
${If} ${Errors}
WriteRegStr HKLM "Software\Decal\Plugins\${APPGUID}" "" "${APPNAME}"
WriteRegDWORD HKLM "Software\Decal\Plugins\${APPGUID}" "Enabled" "1"
WriteRegStr HKLM "Software\Decal\Plugins\${APPGUID}" "Object" "${CLASSNAME}"
WriteRegStr HKLM "Software\Decal\Plugins\${APPGUID}" "Assembly" "${ASSEMBLY}"
WriteRegStr HKLM "Software\Decal\Plugins\${APPGUID}" "Path" "$INSTDIR"
WriteRegStr HKLM "Software\Decal\Plugins\${APPGUID}" "Surrogate" "{71A69713-6593-47EC-0002-0000000DECA1}"
WriteRegStr HKLM "Software\Decal\Plugins\${APPGUID}" "Uninstaller" "${APPNAME}"
${Else}
${IF} $0 != "${APPNAME}"
MESSAGEBOX MB_OK|MB_ICONSTOP "Skipped decal plugin registration. A decal plugin with this GUID already exists ($0), and is not ${APPNAME}."
${ENDIF}
${EndIf}
;Register loader in decal as network filter
ClearErrors
ReadRegStr $0 HKLM "Software\Decal\NetworkFilters\${LOADERGUID}" ""
${If} ${Errors}
WriteRegStr HKLM "Software\Decal\NetworkFilters\${LOADERGUID}" "" "${APPNAME}.Loader"
WriteRegDWORD HKLM "Software\Decal\NetworkFilters\${LOADERGUID}" "Enabled" "0" ; Disabled by default for normal use
WriteRegStr HKLM "Software\Decal\NetworkFilters\${LOADERGUID}" "Object" "${LOADERCLASS}"
WriteRegStr HKLM "Software\Decal\NetworkFilters\${LOADERGUID}" "Assembly" "${LOADERASSEMBLY}"
WriteRegStr HKLM "Software\Decal\NetworkFilters\${LOADERGUID}" "Path" "$INSTDIR"
WriteRegStr HKLM "Software\Decal\NetworkFilters\${LOADERGUID}" "Surrogate" "{71A69713-6593-47EC-0002-0000000DECA1}"
WriteRegStr HKLM "Software\Decal\NetworkFilters\${LOADERGUID}" "Uninstaller" "${APPNAME}"
${Else}
${IF} $0 != "${APPNAME}.Loader"
MESSAGEBOX MB_OK|MB_ICONSTOP "Skipped decal loader registration. A decal network filter with this GUID already exists ($0), and is not ${APPNAME}.Loader."
${ENDIF}
${EndIf}
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "DisplayName" "${APPNAME}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "UninstallString" "$INSTDIR\uninstall.exe"
WriteUninstaller "$INSTDIR\uninstall.exe"
; make sure dotnet 4.8 is installed
Call CheckAndDownloadDotNet48
SectionEnd
; Modern install component descriptions
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
!insertmacro MUI_DESCRIPTION_TEXT ${CoreSection} ""
!insertmacro MUI_FUNCTION_DESCRIPTION_END
;Uninstall section
Section Uninstall
;Remove from registry...
DeleteRegKey HKLM "Software\${SOFTWARECOMPANY}\${APPNAME}"
DeleteRegKey HKLM "Software\Decal\Plugins\${APPGUID}"
DeleteRegKey HKLM "Software\Decal\NetworkFilters\${LOADERGUID}"
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}"
; Delete self
Delete "$INSTDIR\uninstall.exe"
;Clean up
Delete "$INSTDIR\${ASSEMBLY}"
Delete "$INSTDIR\${APPNAME}.pdb"
Delete "$INSTDIR\${LOADERASSEMBLY}"
Delete "$INSTDIR\${APPNAME}.Loader.pdb"
Delete "$INSTDIR\loader_log.txt"
; Delete "$INSTDIR\UtilityBelt.Service.Installer.exe"
;RMDir "$INSTDIR\"
SectionEnd
; eof

View file

@ -0,0 +1,17 @@
param(
[string]$NuGetPackageRoot,
[string]$ProjectDir
)
if ($Env:OS -and $Env:OS -like '*Windows*') {
$makensis = Join-Path $NuGetPackageRoot 'nsis-tool\3.0.8\tools\makensis.exe'
$installer = Join-Path $ProjectDir 'scripts\installer.nsi'
Write-Verbose "Using makensis at $makensis"
& $makensis $installer
}
else {
# Only runs when building on Linux/macOS with makensis in PATH
& makensis "$ProjectDir/scripts/installer.nsi"
}

274
README.md
View file

@ -1,80 +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.
### 🎛️ 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.
- **UI Overlay**: Displays stats and provides buttons to reset stats or toggle rare meta.
- **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)
### 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`
- Config is auto-generated on first run; modify it or use UI/commands to update.
### 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
### Telemetry
- Periodically posts JSON snapshots of position and stats to a configurable endpoint.
- Configure `Endpoint`, `SharedSecret`, and `IntervalSec` in `Telemetry.cs`.
### Building from Source
```bash
# Clone the repository
git clone [repository-url]
cd MosswartMassacre
## Dependencies
- Decal.Adapter (v2.9.8.3)
- Decal.Interop.Core & Decal.Interop.Inject
- VirindiViewService
- Newtonsoft.Json (v13.0.3)
- YamlDotNet (v16.3.0)
# Restore packages and build
nuget restore packages.config
msbuild MosswartMassacre.csproj /p:Configuration=Release /p:Platform=AnyCPU
```
## 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.
## 🎮 Usage
--
_This README provides a high-level overview to get up and running quickly._
### Basic Commands
Access all features through the `/mm` command interface:
```
/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*

View file

@ -0,0 +1,187 @@
namespace Mag.Shared.Constants
{
// https://github.com/ACEmulator/ACE/blob/master/Source/ACE.Entity/Enum/Properties/PropertyBool.cs
public enum BoolValueKey
{
// properties marked as ServerOnly are properties we never saw in PCAPs, from here:
// http://ac.yotesfan.com/ace_object/not_used_enums.php
// source: @OptimShi
// description attributes are used by the weenie editor for a cleaner display name
Undef = 0,
[Ephemeral][ServerOnly]
Stuck = 1,
[Ephemeral]
Open = 2,
Locked = 3,
RotProof = 4,
AllegianceUpdateRequest = 5,
AiUsesMana = 6,
AiUseHumanMagicAnimations = 7,
AllowGive = 8,
CurrentlyAttacking = 9,
AttackerAi = 10,
[ServerOnly]
IgnoreCollisions = 11,
[ServerOnly]
ReportCollisions = 12,
[ServerOnly]
Ethereal = 13,
[ServerOnly]
GravityStatus = 14,
[ServerOnly]
LightsStatus = 15,
[ServerOnly]
ScriptedCollision = 16,
[ServerOnly]
Inelastic = 17,
[ServerOnly][Ephemeral]
Visibility = 18,
[ServerOnly]
Attackable = 19,
SafeSpellComponents = 20,
AdvocateState = 21,
Inscribable = 22,
DestroyOnSell = 23,
UiHidden = 24,
IgnoreHouseBarriers = 25,
HiddenAdmin = 26,
PkWounder = 27,
PkKiller = 28,
NoCorpse = 29,
UnderLifestoneProtection = 30,
ItemManaUpdatePending = 31,
[Ephemeral]
GeneratorStatus = 32,
[Ephemeral]
ResetMessagePending = 33,
DefaultOpen = 34,
DefaultLocked = 35,
DefaultOn = 36,
OpenForBusiness = 37,
IsFrozen = 38,
DealMagicalItems = 39,
LogoffImDead = 40,
ReportCollisionsAsEnvironment = 41,
AllowEdgeSlide = 42,
AdvocateQuest = 43,
[Ephemeral][SendOnLogin]
IsAdmin = 44,
[Ephemeral][SendOnLogin]
IsArch = 45,
[Ephemeral][SendOnLogin]
IsSentinel = 46,
[SendOnLogin]
IsAdvocate = 47,
CurrentlyPoweringUp = 48,
[Ephemeral]
GeneratorEnteredWorld = 49,
NeverFailCasting = 50,
VendorService = 51,
AiImmobile = 52,
DamagedByCollisions = 53,
IsDynamic = 54,
IsHot = 55,
IsAffecting = 56,
AffectsAis = 57,
SpellQueueActive = 58,
[Ephemeral]
GeneratorDisabled = 59,
IsAcceptingTells = 60,
LoggingChannel = 61,
OpensAnyLock = 62,
UnlimitedUse = 63,
GeneratedTreasureItem = 64,
IgnoreMagicResist = 65,
IgnoreMagicArmor = 66,
AiAllowTrade = 67,
[SendOnLogin]
SpellComponentsRequired = 68,
IsSellable = 69,
IgnoreShieldsBySkill = 70,
NoDraw = 71,
ActivationUntargeted = 72,
HouseHasGottenPriorityBootPos = 73,
[Ephemeral]
GeneratorAutomaticDestruction = 74,
HouseHooksVisible = 75,
HouseRequiresMonarch = 76,
HouseHooksEnabled = 77,
HouseNotifiedHudOfHookCount = 78,
AiAcceptEverything = 79,
IgnorePortalRestrictions = 80,
RequiresBackpackSlot = 81,
DontTurnOrMoveWhenGiving = 82,
[ServerOnly]
NpcLooksLikeObject = 83,
IgnoreCloIcons = 84,
AppraisalHasAllowedWielder = 85,
ChestRegenOnClose = 86,
LogoffInMinigame = 87,
PortalShowDestination = 88,
PortalIgnoresPkAttackTimer = 89,
NpcInteractsSilently = 90,
Retained = 91,
IgnoreAuthor = 92,
Limbo = 93,
AppraisalHasAllowedActivator = 94,
ExistedBeforeAllegianceXpChanges = 95,
IsDeaf = 96,
[Ephemeral][SendOnLogin]
IsPsr = 97,
Invincible = 98,
Ivoryable = 99,
Dyable = 100,
CanGenerateRare = 101,
CorpseGeneratedRare = 102,
NonProjectileMagicImmune = 103,
[SendOnLogin]
ActdReceivedItems = 104,
Unknown105 = 105,
[Ephemeral]
FirstEnterWorldDone = 106,
RecallsDisabled = 107,
RareUsesTimer = 108,
ActdPreorderReceivedItems = 109,
Afk = 110,
IsGagged = 111,
ProcSpellSelfTargeted = 112,
IsAllegianceGagged = 113,
EquipmentSetTriggerPiece = 114,
Uninscribe = 115,
WieldOnUse = 116,
ChestClearedWhenClosed = 117,
NeverAttack = 118,
SuppressGenerateEffect = 119,
TreasureCorpse = 120,
EquipmentSetAddLevel = 121,
BarberActive = 122,
TopLayerPriority = 123,
NoHeldItemShown = 124,
LoginAtLifestone = 125,
OlthoiPk = 126,
[SendOnLogin]
Account15Days = 127,
HadNoVitae = 128,
NoOlthoiTalk = 129,
AutowieldLeft = 130,
// ACE Specific
/* custom */
[ServerOnly]
LinkedPortalOneSummon = 9001,
[ServerOnly]
LinkedPortalTwoSummon = 9002,
[ServerOnly]
HouseEvicted = 9003,
[ServerOnly]
UntrainedSkills = 9004,
// Decal Specific
Lockable_Decal = 201326592,
Inscribable_Decal = 201326593,
}
}

View file

@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
namespace Mag.Shared.Constants
{
/// <summary>
/// This is the mapping for LongValueKey.Coverage.
/// It represents what body parts an armor piece covers when used in defensive/armor calculations.
/// </summary>
[Flags]
public enum CoverageMask
{
None = 0,
Unknown = 0x00000001, // Original pants abdomen?
UnderwearUpperLegs = 0x00000002, // I think... 0x13 = Abdomen/UpperLegs
UnderwearLowerLegs = 0x00000004, // I think... 0x16 = Abdomen/UpperLegs/LowerLegs
UnderwearChest = 0x00000008,
UnderwearAbdomen = 0x00000010, // Original shirt abdomen?
UnderwearUpperArms = 0x00000020,
UnderwearLowerArms = 0x00000040,
// = 0x00000080,
OuterwearUpperLegs = 0x00000100,
OuterwearLowerLegs = 0x00000200,
OuterwearChest = 0x00000400,
OuterwearAbdomen = 0x00000800,
OuterwearUpperArms = 0x00001000,
OuterwearLowerArms = 0x00002000,
Head = 0x00004000,
Hands = 0x00008000,
Feet = 0x00010000,
Cloak = 0x00020000,
}
public enum CoverageMaskHelper : uint
{
// for server comparison only
Underwear = CoverageMask.UnderwearUpperLegs | CoverageMask.UnderwearLowerLegs | CoverageMask.UnderwearChest | CoverageMask.UnderwearAbdomen | CoverageMask.UnderwearUpperArms | CoverageMask.UnderwearLowerArms,
Outerwear = CoverageMask.OuterwearUpperLegs | CoverageMask.OuterwearLowerLegs | CoverageMask.OuterwearChest | CoverageMask.OuterwearAbdomen | CoverageMask.OuterwearUpperArms | CoverageMask.OuterwearLowerArms | CoverageMask.Head | CoverageMask.Hands | CoverageMask.Feet,
UnderwearLegs = CoverageMask.UnderwearUpperLegs | CoverageMask.UnderwearLowerLegs,
UnderwearArms = CoverageMask.UnderwearUpperArms | CoverageMask.UnderwearLowerArms,
OuterwearLegs = CoverageMask.OuterwearUpperLegs | CoverageMask.OuterwearLowerLegs,
OuterwearArms = CoverageMask.OuterwearUpperArms | CoverageMask.OuterwearLowerArms,
// exclude abdomen for searching
UnderwearShirt = CoverageMask.UnderwearChest | CoverageMask.UnderwearUpperArms | CoverageMask.UnderwearLowerArms,
UnderwearPants = CoverageMask.UnderwearUpperLegs | CoverageMask.UnderwearLowerLegs
}
public static class CoverageMaskExtensions
{
public static int GetTotalBitsSet(this CoverageMask value)
{
int slotFlags = (int)value;
int bitsSet = 0;
while (slotFlags != 0)
{
if ((slotFlags & 1) == 1)
bitsSet++;
slotFlags >>= 1;
}
return bitsSet;
}
public static bool IsBodyArmor(this CoverageMask value) { return ((int)value & 0x0001FF00) != 0; }
public static bool IsRobe(this CoverageMask value) { return ((int)value == 0x00013F00); }
public static bool IsUnderwear(this CoverageMask value) { return ((int)value & 0x0000007F) != 0; }
public static bool IsShirt(this CoverageMask value) { return ((int)value & 0x00000078) != 0; }
public static bool IsPants(this CoverageMask value) { return ((int)value & 0x00000017) != 0; }
public static List<CoverageMask> ReductionOptions(this CoverageMask value)
{
List<CoverageMask> options = new List<CoverageMask>();
if (value.GetTotalBitsSet() <= 1 || !value.IsBodyArmor() || value.IsRobe())
options.Add(value);
else
{
if (value == (CoverageMask.OuterwearUpperArms | CoverageMask.OuterwearLowerArms))
{
options.Add(CoverageMask.OuterwearUpperArms);
options.Add(CoverageMask.OuterwearLowerArms);
}
else if (value == (CoverageMask.OuterwearUpperLegs | CoverageMask.OuterwearLowerLegs))
{
options.Add(CoverageMask.OuterwearUpperLegs);
options.Add(CoverageMask.OuterwearLowerLegs);
}
else if (value == (CoverageMask.OuterwearLowerLegs | CoverageMask.Feet))
options.Add(CoverageMask.Feet);
else if (value == (CoverageMask.OuterwearChest | CoverageMask.OuterwearAbdomen))
options.Add(CoverageMask.OuterwearChest);
else if (value == (CoverageMask.OuterwearChest | CoverageMask.OuterwearAbdomen | CoverageMask.OuterwearUpperArms))
options.Add(CoverageMask.OuterwearChest);
else if (value == (CoverageMask.OuterwearChest | CoverageMask.OuterwearUpperArms | CoverageMask.OuterwearLowerArms))
options.Add(CoverageMask.OuterwearChest);
else if (value == (CoverageMask.OuterwearChest | CoverageMask.OuterwearUpperArms))
options.Add(CoverageMask.OuterwearChest);
else if (value == (CoverageMask.OuterwearAbdomen | CoverageMask.OuterwearUpperLegs | CoverageMask.OuterwearLowerLegs))
{
options.Add(CoverageMask.OuterwearAbdomen);
options.Add(CoverageMask.OuterwearUpperLegs);
options.Add(CoverageMask.OuterwearLowerLegs);
}
else if (value == (CoverageMask.OuterwearChest | CoverageMask.OuterwearAbdomen | CoverageMask.OuterwearUpperArms | CoverageMask.OuterwearLowerArms))
options.Add(CoverageMask.OuterwearChest);
else if (value == (CoverageMask.OuterwearAbdomen | CoverageMask.OuterwearUpperLegs))
{
// This is a emu piece that follows the pre-2010 retail guidelines
// https://asheron.fandom.com/wiki/Announcements_-_2010/04_-_Shedding_Skin
// For now, we assume only abdomen reduction
options.Add(CoverageMask.OuterwearAbdomen);
}
else
throw new Exception("Unable to determine reduction paths for CoverageMask of " + value);
}
return options;
}
}
}

View file

@ -0,0 +1,436 @@
using System.Collections.Generic;
namespace Mag.Shared.Constants
{
public static class Dictionaries
{
/// <summary>
/// Returns a dictionary of skill ids vs names
/// </summary>
/// <returns></returns>
public static readonly Dictionary<int, string> SkillInfo = new Dictionary<int, string>
{
// This list was taken from the Alinco source
{ 0x1, "Axe" },
{ 0x2, "Bow" },
{ 0x3, "Crossbow" },
{ 0x4, "Dagger" },
{ 0x5, "Mace" },
{ 0x6, "Melee Defense" },
{ 0x7, "Missile Defense" },
{ 0x8, "Sling" },
{ 0x9, "Spear" },
{ 0xA, "Staff" },
{ 0xB, "Sword" },
{ 0xC, "Thrown Weapons" },
{ 0xD, "Unarmed Combat" },
{ 0xE, "Arcane Lore" },
{ 0xF, "Magic Defense" },
{ 0x10, "Mana Conversion" },
{ 0x12, "Item Tinkering" },
{ 0x13, "Assess Person" },
{ 0x14, "Deception" },
{ 0x15, "Healing" },
{ 0x16, "Jump" },
{ 0x17, "Lockpick" },
{ 0x18, "Run" },
{ 0x1B, "Assess Creature" },
{ 0x1C, "Weapon Tinkering" },
{ 0x1D, "Armor Tinkering" },
{ 0x1E, "Magic Item Tinkering" },
{ 0x1F, "Creature Enchantment" },
{ 0x20, "Item Enchantment" },
{ 0x21, "Life Magic" },
{ 0x22, "War Magic" },
{ 0x23, "Leadership" },
{ 0x24, "Loyalty" },
{ 0x25, "Fletching" },
{ 0x26, "Alchemy" },
{ 0x27, "Cooking" },
{ 0x28, "Salvaging" },
{ 0x29, "Two Handed Combat" },
{ 0x2A, "Gearcraft"},
{ 0x2B, "Void" },
{ 0x2C, "Heavy Weapons" },
{ 0x2D, "Light Weapons" },
{ 0x2E, "Finesse Weapons" },
{ 0x2F, "Missile Weapons" },
{ 0x30, "Shield" },
{ 0x31, "Dual Wield" },
{ 0x32, "Recklessness" },
{ 0x33, "Sneak Attack" },
{ 0x34, "Dirty Fighting" },
{ 0x35, "Challenge" },
{ 0x36, "Summoning" },
};
/// <summary>
/// Returns a dictionary of mastery ids vs names
/// </summary>
/// <returns></returns>
public static Dictionary<int, string> MasteryInfo = new Dictionary<int, string>
{
{ 1, "Unarmed Weapon" },
{ 2, "Sword" },
{ 3, "Axe" },
{ 4, "Mace" },
{ 5, "Spear" },
{ 6, "Dagger" },
{ 7, "Staff" },
{ 8, "Bow" },
{ 9, "Crossbow" },
{ 10, "Thrown" },
{ 11, "Two Handed Combat" },
};
/// <summary>
/// Returns a dictionary of attribute set ids vs names
/// </summary>
/// <returns></returns>
public static Dictionary<int, string> AttributeSetInfo = new Dictionary<int, string>
{
// This list was taken from Virindi Tank Loot Editor
// 01
{ 02, "Test"},
// 03
{ 04, "Carraida's Benediction"},
{ 05, "Noble Relic Set" },
{ 06, "Ancient Relic Set" },
{ 07, "Relic Alduressa Set" },
{ 08, "Shou-jen Set" },
{ 09, "Empyrean Rings Set" },
{ 10, "Arm, Mind, Heart Set" },
{ 11, "Coat of the Perfect Light Set" },
{ 12, "Leggings of Perfect Light Set" },
{ 13, "Soldier's Set" },
{ 14, "Adept's Set" },
{ 15, "Archer's Set" },
{ 16, "Defender's Set" },
{ 17, "Tinker's Set" },
{ 18, "Crafter's Set" },
{ 19, "Hearty Set" },
{ 20, "Dexterous Set" },
{ 21, "Wise Set" },
{ 22, "Swift Set" },
{ 23, "Hardenend Set" },
{ 24, "Reinforced Set" },
{ 25, "Interlocking Set" },
{ 26, "Flame Proof Set" },
{ 27, "Acid Proof Set" },
{ 28, "Cold Proof Set" },
{ 29, "Lightning Proof Set" },
{ 30, "Dedication Set" },
{ 31, "Gladiatorial Clothing Set" },
{ 32, "Ceremonial Clothing" },
{ 33, "Protective Clothing" },
{ 34, "Noobie Armor" },
{ 35, "Sigil of Defense" },
{ 36, "Sigil of Destruction" },
{ 37, "Sigil of Fury" },
{ 38, "Sigil of Growth" },
{ 39, "Sigil of Vigor" },
{ 40, "Heroic Protector Set" },
{ 41, "Heroic Destroyer Set" },
{ 42, "Olthoi Armor D Red" },
{ 43, "Olthoi Armor C Rat" },
{ 44, "Olthoi Armor C Red" },
{ 45, "Olthoi Armor D Rat" },
{ 46, "Upgraded Relic Alduressa Set" },
{ 47, "Upgraded Ancient Relic Set" },
{ 48, "Upgraded Noble Relic Set" },
{ 49, "Weave of Alchemy" },
{ 50, "Weave of Arcane Lore" },
{ 51, "Weave of Armor Tinkering" },
{ 52, "Weave of Assess Person" },
{ 53, "Weave of Light Weapons" },
{ 54, "Weave of Missile Weapons" },
{ 55, "Weave of Cooking" },
{ 56, "Weave of Creature Enchantment" },
{ 57, "Weave of Missile Weapons" },
{ 58, "Weave of Finesse" },
{ 59, "Weave of Deception" },
{ 60, "Weave of Fletching" },
{ 61, "Weave of Healing" },
{ 62, "Weave of Item Enchantment" },
{ 63, "Weave of Item Tinkering" },
{ 64, "Weave of Leadership" },
{ 65, "Weave of Life Magic" },
{ 66, "Weave of Loyalty" },
{ 67, "Weave of Light Weapons" },
{ 68, "Weave of Magic Defense" },
{ 69, "Weave of Magic Item Tinkering" },
{ 70, "Weave of Mana Conversion" },
{ 71, "Weave of Melee Defense" },
{ 72, "Weave of Missile Defense" },
{ 73, "Weave of Salvaging" },
{ 74, "Weave of Light Weapons" },
{ 75, "Weave of Light Weapons" },
{ 76, "Weave of Heavy Weapons" },
{ 77, "Weave of Missile Weapons" },
{ 78, "Weave of Two Handed Combat" },
{ 79, "Weave of Light Weapons" },
{ 80, "Weave of Void Magic" },
{ 81, "Weave of War Magic" },
{ 82, "Weave of Weapon Tinkering" },
{ 83, "Weave of Assess Creature " },
{ 84, "Weave of Dirty Fighting" },
{ 85, "Weave of Dual Wield" },
{ 86, "Weave of Recklessness" },
{ 87, "Weave of Shield" },
{ 88, "Weave of Sneak Attack" },
{ 89, "Ninja_New" },
{ 90, "Weave of Summoning" },
{ 91, "Shrouded Soul" },
{ 92, "Darkened Mind" },
{ 93, "Clouded Spirit" },
{ 94, "Minor Stinging Shrouded Soul" },
{ 95, "Minor Sparking Shrouded Soul" },
{ 96, "Minor Smoldering Shrouded Soul" },
{ 97, "Minor Shivering Shrouded Soul" },
{ 98, "Minor Stinging Darkened Mind" },
{ 99, "Minor Sparking Darkened Mind" },
{ 100, "Minor Smoldering Darkened Mind" },
{ 101, "Minor Shivering Darkened Mind" },
{ 102, "Minor Stinging Clouded Spirit" },
{ 103, "Minor Sparking Clouded Spirit" },
{ 104, "Minor Smoldering Clouded Spirit" },
{ 105, "Minor Shivering Clouded Spirit" },
{ 106, "Major Stinging Shrouded Soul" },
{ 107, "Major Sparking Shrouded Soul" },
{ 108, "Major Smoldering Shrouded Soul" },
{ 109, "Major Shivering Shrouded Soul" },
{ 110, "Major Stinging Darkened Mind" },
{ 111, "Major Sparking Darkened Mind" },
{ 112, "Major Smoldering Darkened Mind" },
{ 113, "Major Shivering Darkened Mind" },
{ 114, "Major Stinging Clouded Spirit" },
{ 115, "Major Sparking Clouded Spirit" },
{ 116, "Major Smoldering Clouded Spirit" },
{ 117, "Major Shivering Clouded Spirit" },
{ 118, "Blackfire Stinging Shrouded Soul" },
{ 119, "Blackfire Sparking Shrouded Soul" },
{ 120, "Blackfire Smoldering Shrouded Soul" },
{ 121, "Blackfire Shivering Shrouded Soul" },
{ 122, "Blackfire Stinging Darkened Mind" },
{ 123, "Blackfire Sparking Darkened Mind" },
{ 124, "Blackfire Smoldering Darkened Mind" },
{ 125, "Blackfire Shivering Darkened Mind" },
{ 126, "Blackfire Stinging Clouded Spirit" },
{ 127, "Blackfire Sparking Clouded Spirit" },
{ 128, "Blackfire Smoldering Clouded Spirit" },
{ 129, "Blackfire Shivering Clouded Spirit" },
{ 130, "Shimmering Shadows" },
{ 131, "Brown Society Locket" },
{ 132, "Yellow Society Locket" },
{ 133, "Red Society Band" },
{ 134, "Green Society Band" },
{ 135, "Purple Society Band" },
{ 136, "Blue Society Band" },
{ 137, "Gauntlet Garb" },
{ 138, "UNKNOWN_138" }, // Possibly Paragon Missile Weapons
{ 139, "UNKNOWN_139" }, // Possibly Paragon Casters
{ 140, "UNKNOWN_140" }, // Possibly Paragon Melee Weapons
};
/// <summary>
/// Returns a dictionary of material ids vs names
/// </summary>
/// <returns></returns>
public static Dictionary<int, string> MaterialInfo = new Dictionary<int, string>
{
{ 1, "Ceramic" },
{ 2, "Porcelain" },
// 3
{ 4, "Linen" },
{ 5, "Satin" },
{ 6, "Silk" },
{ 7, "Velvet" },
{ 8, "Wool" },
// 9
{ 10, "Agate" },
{ 11, "Amber" },
{ 12, "Amethyst" },
{ 13, "Aquamarine" },
{ 14, "Azurite" },
{ 15, "Black Garnet" },
{ 16, "Black Opal" },
{ 17, "Bloodstone" },
{ 18, "Carnelian" },
{ 19, "Citrine" },
{ 20, "Diamond" },
{ 21, "Emerald" },
{ 22, "Fire Opal" },
{ 23, "Green Garnet" },
{ 24, "Green Jade" },
{ 25, "Hematite" },
{ 26, "Imperial Topaz" },
{ 27, "Jet" },
{ 28, "Lapis Lazuli" },
{ 29, "Lavender Jade" },
{ 30, "Malachite" },
{ 31, "Moonstone" },
{ 32, "Onyx" },
{ 33, "Opal" },
{ 34, "Peridot" },
{ 35, "Red Garnet" },
{ 36, "Red Jade" },
{ 37, "Rose Quartz" },
{ 38, "Ruby" },
{ 39, "Sapphire" },
{ 40, "Smokey Quartz" },
{ 41, "Sunstone" },
{ 42, "Tiger Eye" },
{ 43, "Tourmaline" },
{ 44, "Turquoise" },
{ 45, "White Jade" },
{ 46, "White Quartz" },
{ 47, "White Sapphire" },
{ 48, "Yellow Garnet" },
{ 49, "Yellow Topaz" },
{ 50, "Zircon" },
{ 51, "Ivory" },
{ 52, "Leather" },
{ 53, "Armoredillo Hide" },
{ 54, "Gromnie Hide" },
{ 55, "Reed Shark Hide" },
// 56
{ 57, "Brass" },
{ 58, "Bronze" },
{ 59, "Copper" },
{ 60, "Gold" },
{ 61, "Iron" },
{ 62, "Pyreal" },
{ 63, "Silver" },
{ 64, "Steel" },
// 65
{ 66, "Alabaster" },
{ 67, "Granite" },
{ 68, "Marble" },
{ 69, "Obsidian" },
{ 70, "Sandstone" },
{ 71, "Serpentine" },
{ 73, "Ebony" },
{ 74, "Mahogany" },
{ 75, "Oak" },
{ 76, "Pine" },
{ 77, "Teak" },
};
public struct SpellInfo<T>
{
public readonly int Key;
public readonly T Change;
public readonly T Bonus;
public SpellInfo(int key, T change, T bonus = default(T))
{
Key = key;
Change = change;
Bonus = bonus;
}
}
// Taken from Decal.Adapter.Wrappers.LongValueKey
const int LongValueKey_MaxDamage = 218103842;
const int LongValueKey_ArmorLevel = 28;
public static readonly Dictionary<int, SpellInfo<int>> LongValueKeySpellEffects = new Dictionary<int, SpellInfo<int>>()
{
// In 2012 they removed these item spells and converted them to auras that are cast on the player, not on the item.
{ 1616, new SpellInfo<int>(LongValueKey_MaxDamage, 20)}, // Blood Drinker VI
{ 2096, new SpellInfo<int>(LongValueKey_MaxDamage, 22)}, // Infected Caress
//{ 5183, new SpellInfo<LongValueKey>(LongValueKey_MaxDamage, 22)}, // Incantation of Blood Drinker Pre Feb-2013
//{ 4395, new SpellInfo<LongValueKey>(LongValueKey_MaxDamage, 24, 2)}, // Incantation of Blood Drinker, this spell on the item adds 2 more points of damage over a user casted 8 Pre Feb-2013
{ 5183, new SpellInfo<int>(LongValueKey_MaxDamage, 24)}, // Incantation of Blood Drinker Post Feb-2013
{ 4395, new SpellInfo<int>(LongValueKey_MaxDamage, 24)}, // Incantation of Blood Drinker Post Feb-2013
{ 2598, new SpellInfo<int>(LongValueKey_MaxDamage, 2, 2)}, // Minor Blood Thirst
{ 2586, new SpellInfo<int>(LongValueKey_MaxDamage, 4, 4)}, // Major Blood Thirst
{ 4661, new SpellInfo<int>(LongValueKey_MaxDamage, 7, 7)}, // Epic Blood Thirst
{ 6089, new SpellInfo<int>(LongValueKey_MaxDamage, 10, 10)}, // Legendary Blood Thirst
{ 3688, new SpellInfo<int>(LongValueKey_MaxDamage, 300)}, // Prodigal Blood Drinker
{ 1486, new SpellInfo<int>(LongValueKey_ArmorLevel, 200)}, // Impenetrability VI
{ 2108, new SpellInfo<int>(LongValueKey_ArmorLevel, 220)}, // Brogard's Defiance
{ 4407, new SpellInfo<int>(LongValueKey_ArmorLevel, 240)}, // Incantation of Impenetrability
{ 2604, new SpellInfo<int>(LongValueKey_ArmorLevel, 20, 20)}, // Minor Impenetrability
{ 2592, new SpellInfo<int>(LongValueKey_ArmorLevel, 40, 40)}, // Major Impenetrability
{ 4667, new SpellInfo<int>(LongValueKey_ArmorLevel, 60, 60)}, // Epic Impenetrability
{ 6095, new SpellInfo<int>(LongValueKey_ArmorLevel, 80, 80)}, // Legendary Impenetrability
};
// Taken from Decal.Adapter.Wrappers.DoubleValueKey
const int DoubleValueKey_ElementalDamageVersusMonsters = 152;
const int DoubleValueKey_AttackBonus = 167772172;
const int DoubleValueKey_MeleeDefenseBonus = 29;
const int DoubleValueKey_ManaCBonus = 144;
public static readonly Dictionary<int, SpellInfo<double>> DoubleValueKeySpellEffects = new Dictionary<int, SpellInfo<double>>()
{
// In 2012 they removed these item spells and converted them to auras that are cast on the player, not on the item.
{ 3258, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .06)}, // Spirit Drinker VI
{ 3259, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .07)}, // Infected Spirit Caress
//{ 5182, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .07)}, // Incantation of Spirit Drinker Pre Feb-2013
//{ 4414, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .08, .01)}, // Incantation of Spirit Drinker, this spell on the item adds 1 more % of damage over a user casted 8 Pre Feb-2013
{ 5182, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .08)}, // Incantation of Spirit Drinker Post Feb-2013
{ 4414, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .08)}, // Incantation of Spirit Drinker, this spell on the item adds 1 more % of damage over a user casted 8 Post Feb-2013
{ 3251, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .01, .01)}, // Minor Spirit Thirst
{ 3250, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .03, .03)}, // Major Spirit Thirst
{ 4670, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .05, .05)}, // Epic Spirit Thirst
{ 6098, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .07, .07)}, // Legendary Spirit Thirst
{ 3735, new SpellInfo<double>(DoubleValueKey_ElementalDamageVersusMonsters, .15)}, // Prodigal Spirit Drinker
// In 2012 they removed these item spells and converted them to auras that are cast on the player, not on the item.
{ 1592, new SpellInfo<double>(DoubleValueKey_AttackBonus, .15)}, // Heart Seeker VI
{ 2106, new SpellInfo<double>(DoubleValueKey_AttackBonus, .17)}, // Elysa's Sight
{ 4405, new SpellInfo<double>(DoubleValueKey_AttackBonus, .20)}, // Incantation of Heart Seeker
{ 2603, new SpellInfo<double>(DoubleValueKey_AttackBonus, .03, .03)}, // Minor Heart Thirst
{ 2591, new SpellInfo<double>(DoubleValueKey_AttackBonus, .05, .05)}, // Major Heart Thirst
{ 4666, new SpellInfo<double>(DoubleValueKey_AttackBonus, .07, .07)}, // Epic Heart Thirst
{ 6094, new SpellInfo<double>(DoubleValueKey_AttackBonus, .09, .09)}, // Legendary Heart Thirst
// In 2012 they removed these item spells and converted them to auras that are cast on the player, not on the item.
{ 1605, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .15)}, // Defender VI
{ 2101, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .17)}, // Cragstone's Will
//{ 4400, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .17)}, // Incantation of Defender Pre Feb-2013
{ 4400, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .20)}, // Incantation of Defender Post Feb-2013
{ 2600, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .03, .03)}, // Minor Defender
{ 3985, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .04, .04)}, // Mukkir Sense
{ 2588, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .05, .05)}, // Major Defender
{ 4663, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .07, .07)}, // Epic Defender
{ 6091, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .09, .09)}, // Legendary Defender
{ 3699, new SpellInfo<double>(DoubleValueKey_MeleeDefenseBonus, .25)}, // Prodigal Defender
// In 2012 they removed these item spells and converted them to auras that are cast on the player, not on the item.
{ 1480, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.60)}, // Hermetic Link VI
{ 2117, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.70)}, // Mystic's Blessing
{ 4418, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.80)}, // Incantation of Hermetic Link
{ 3201, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.05, 1.05)}, // Feeble Hermetic Link
{ 3199, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.10, 1.10)}, // Minor Hermetic Link
{ 3202, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.15, 1.15)}, // Moderate Hermetic Link
{ 3200, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.20, 1.20)}, // Major Hermetic Link
{ 6086, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.25, 1.25)}, // Epic Hermetic Link
{ 6087, new SpellInfo<double>(DoubleValueKey_ManaCBonus, 1.30, 1.30)}, // Legendary Hermetic Link
};
}
}

View file

@ -0,0 +1,266 @@
namespace Mag.Shared.Constants
{
// https://github.com/ACEmulator/ACE/blob/master/Source/ACE.Entity/Enum/Properties/PropertyFloat.cs
public enum DoubleValueKey
{
// properties marked as ServerOnly are properties we never saw in PCAPs, from here:
// http://ac.yotesfan.com/ace_object/not_used_enums.php
// source: @OptimShi
// description attributes are used by the weenie editor for a cleaner display name
Undef = 0,
HeartbeatInterval = 1,
[Ephemeral]
HeartbeatTimestamp = 2,
HealthRate = 3,
StaminaRate = 4,
ManaRate = 5,
HealthUponResurrection = 6,
StaminaUponResurrection = 7,
ManaUponResurrection = 8,
StartTime = 9,
StopTime = 10,
ResetInterval = 11,
Shade = 12,
ArmorModVsSlash = 13,
ArmorModVsPierce = 14,
ArmorModVsBludgeon = 15,
ArmorModVsCold = 16,
ArmorModVsFire = 17,
ArmorModVsAcid = 18,
ArmorModVsElectric = 19,
CombatSpeed = 20,
WeaponLength = 21,
DamageVariance = 22,
CurrentPowerMod = 23,
AccuracyMod = 24,
StrengthMod = 25,
MaximumVelocity = 26,
RotationSpeed = 27,
MotionTimestamp = 28,
WeaponDefense = 29,
WimpyLevel = 30,
VisualAwarenessRange = 31,
AuralAwarenessRange = 32,
PerceptionLevel = 33,
PowerupTime = 34,
MaxChargeDistance = 35,
ChargeSpeed = 36,
BuyPrice = 37,
SellPrice = 38,
DefaultScale = 39,
LockpickMod = 40,
RegenerationInterval = 41,
RegenerationTimestamp = 42,
GeneratorRadius = 43,
TimeToRot = 44,
DeathTimestamp = 45,
PkTimestamp = 46,
VictimTimestamp = 47,
LoginTimestamp = 48,
CreationTimestamp = 49,
MinimumTimeSincePk = 50,
DeprecatedHousekeepingPriority = 51,
AbuseLoggingTimestamp = 52,
LastPortalTeleportTimestamp = 53,
UseRadius = 54,
HomeRadius = 55,
ReleasedTimestamp = 56,
MinHomeRadius = 57,
Facing = 58,
ResetTimestamp = 59,
LogoffTimestamp = 60,
EconRecoveryInterval = 61,
WeaponOffense = 62,
DamageMod = 63,
ResistSlash = 64,
ResistPierce = 65,
ResistBludgeon = 66,
ResistFire = 67,
ResistCold = 68,
ResistAcid = 69,
ResistElectric = 70,
ResistHealthBoost = 71,
ResistStaminaDrain = 72,
ResistStaminaBoost = 73,
ResistManaDrain = 74,
ResistManaBoost = 75,
[Ephemeral]
Translucency = 76,
PhysicsScriptIntensity = 77,
Friction = 78,
Elasticity = 79,
AiUseMagicDelay = 80,
ItemMinSpellcraftMod = 81,
ItemMaxSpellcraftMod = 82,
ItemRankProbability = 83,
Shade2 = 84,
Shade3 = 85,
Shade4 = 86,
ItemEfficiency = 87,
ItemManaUpdateTimestamp = 88,
SpellGestureSpeedMod = 89,
SpellStanceSpeedMod = 90,
AllegianceAppraisalTimestamp = 91,
PowerLevel = 92,
AccuracyLevel = 93,
AttackAngle = 94,
AttackTimestamp = 95,
CheckpointTimestamp = 96,
SoldTimestamp = 97,
UseTimestamp = 98,
UseLockTimestamp = 99,
HealkitMod = 100,
FrozenTimestamp = 101,
HealthRateMod = 102,
AllegianceSwearTimestamp = 103,
ObviousRadarRange = 104,
HotspotCycleTime = 105,
HotspotCycleTimeVariance = 106,
SpamTimestamp = 107,
SpamRate = 108,
BondWieldedTreasure = 109,
BulkMod = 110,
SizeMod = 111,
GagTimestamp = 112,
GeneratorUpdateTimestamp = 113,
DeathSpamTimestamp = 114,
DeathSpamRate = 115,
WildAttackProbability = 116,
FocusedProbability = 117,
CrashAndTurnProbability = 118,
CrashAndTurnRadius = 119,
CrashAndTurnBias = 120,
GeneratorInitialDelay = 121,
AiAcquireHealth = 122,
AiAcquireStamina = 123,
AiAcquireMana = 124,
/// <summary>
/// this had a default of "1" - leaving comment to investigate potential options for defaulting these things (125)
/// </summary>
[SendOnLogin]
ResistHealthDrain = 125,
LifestoneProtectionTimestamp = 126,
AiCounteractEnchantment = 127,
AiDispelEnchantment = 128,
TradeTimestamp = 129,
AiTargetedDetectionRadius = 130,
EmotePriority = 131,
[Ephemeral]
LastTeleportStartTimestamp = 132,
EventSpamTimestamp = 133,
EventSpamRate = 134,
InventoryOffset = 135,
CriticalMultiplier = 136,
ManaStoneDestroyChance = 137,
SlayerDamageBonus = 138,
AllegianceInfoSpamTimestamp = 139,
AllegianceInfoSpamRate = 140,
NextSpellcastTimestamp = 141,
[Ephemeral]
AppraisalRequestedTimestamp = 142,
AppraisalHeartbeatDueTimestamp = 143,
ManaConversionMod = 144,
LastPkAttackTimestamp = 145,
FellowshipUpdateTimestamp = 146,
CriticalFrequency = 147,
LimboStartTimestamp = 148,
WeaponMissileDefense = 149,
WeaponMagicDefense = 150,
IgnoreShield = 151,
ElementalDamageMod = 152,
StartMissileAttackTimestamp = 153,
LastRareUsedTimestamp = 154,
IgnoreArmor = 155,
ProcSpellRate = 156,
ResistanceModifier = 157,
AllegianceGagTimestamp = 158,
AbsorbMagicDamage = 159,
CachedMaxAbsorbMagicDamage = 160,
GagDuration = 161,
AllegianceGagDuration = 162,
[SendOnLogin]
GlobalXpMod = 163,
HealingModifier = 164,
ArmorModVsNether = 165,
ResistNether = 166,
CooldownDuration = 167,
[SendOnLogin]
WeaponAuraOffense = 168,
[SendOnLogin]
WeaponAuraDefense = 169,
[SendOnLogin]
WeaponAuraElemental = 170,
[SendOnLogin]
WeaponAuraManaConv = 171,
// ACE Specific
[ServerOnly]
PCAPRecordedWorkmanship = 8004,
[ServerOnly]
PCAPRecordedVelocityX = 8010,
[ServerOnly]
PCAPRecordedVelocityY = 8011,
[ServerOnly]
PCAPRecordedVelocityZ = 8012,
[ServerOnly]
PCAPRecordedAccelerationX = 8013,
[ServerOnly]
PCAPRecordedAccelerationY = 8014,
[ServerOnly]
PCAPRecordedAccelerationZ = 8015,
[ServerOnly]
PCAPRecordeOmegaX = 8016,
[ServerOnly]
PCAPRecordeOmegaY = 8017,
[ServerOnly]
PCAPRecordeOmegaZ = 8018,
// Decal Specific
SlashProt_Decal = 167772160,
PierceProt_Decal = 167772161,
BludgeonProt_Decal = 167772162,
AcidProt_Decal = 167772163,
LightningProt_Decal = 167772164,
FireProt_Decal = 167772165,
ColdProt_Decal = 167772166,
Heading_Decal = 167772167,
ApproachDistance_Decal = 167772168,
SalvageWorkmanship_Decal = 167772169,
Scale_Decal = 167772170,
Variance_Decal = 167772171,
AttackBonus_Decal = 167772172,
Range_Decal = 167772173,
DamageBonus_Decal = 167772174,
}
public static class DoubleValueKeyTools
{
/// <summary>
/// Converts a decal specific IntValueKey to the actual IntValueKey.
/// If this is not an IntValueKey, 0 will be returned.
/// </summary>
public static uint ConvertToDouble(DoubleValueKey input)
{
if (input == DoubleValueKey.SlashProt_Decal) return (int)DoubleValueKey.ArmorModVsSlash;
if (input == DoubleValueKey.PierceProt_Decal) return (int)DoubleValueKey.ArmorModVsPierce;
if (input == DoubleValueKey.BludgeonProt_Decal) return (int)DoubleValueKey.ArmorModVsBludgeon;
if (input == DoubleValueKey.AcidProt_Decal) return (int)DoubleValueKey.ArmorModVsAcid;
if (input == DoubleValueKey.LightningProt_Decal) return (int)DoubleValueKey.ArmorModVsElectric;
if (input == DoubleValueKey.FireProt_Decal) return (int)DoubleValueKey.ArmorModVsFire;
if (input == DoubleValueKey.ColdProt_Decal) return (int)DoubleValueKey.ArmorModVsCold;
if (input == DoubleValueKey.ApproachDistance_Decal) return (int)DoubleValueKey.UseRadius;
if (input == DoubleValueKey.Scale_Decal) return (int)DoubleValueKey.DefaultScale;
if (input == DoubleValueKey.Variance_Decal) return (int)DoubleValueKey.DamageVariance;
if (input == DoubleValueKey.AttackBonus_Decal) return (int)DoubleValueKey.WeaponOffense;;
if (input == DoubleValueKey.Range_Decal) return (int)DoubleValueKey.MaximumVelocity;
if (input == DoubleValueKey.DamageBonus_Decal) return (int)DoubleValueKey.DamageMod;
return 0;
}
}
}

View file

@ -0,0 +1,11 @@
using System;
namespace Mag.Shared.Constants
{
/// <summary>
/// These are properties that aren't saved to the shard.
/// </summary>
public class EphemeralAttribute : Attribute
{
}
}

View file

@ -0,0 +1,126 @@
using System;
namespace Mag.Shared.Constants
{
/// <summary>
/// This is the mapping for LongValueKey.EquippableSlots.
/// It represents where you can drag items to on your paper doll.
/// </summary>
[Flags]
public enum EquipMask : uint
{
None = 0x00000000,
HeadWear = 0x00000001,
ChestWear = 0x00000002,
AbdomenWear = 0x00000004,
UpperArmWear = 0x00000008,
LowerArmWear = 0x00000010,
HandWear = 0x00000020,
UpperLegWear = 0x00000040,
LowerLegWear = 0x00000080,
FootWear = 0x00000100,
ChestArmor = 0x00000200,
AbdomenArmor = 0x00000400,
UpperArmArmor = 0x00000800,
LowerArmArmor = 0x00001000,
UpperLegArmor = 0x00002000,
LowerLegArmor = 0x00004000,
NeckWear = 0x00008000,
WristWearLeft = 0x00010000,
WristWearRight = 0x00020000,
FingerWearLeft = 0x00040000,
FingerWearRight = 0x00080000,
MeleeWeapon = 0x00100000,
Shield = 0x00200000,
MissileWeapon = 0x00400000,
MissileAmmo = 0x00800000,
Held = 0x01000000,
TwoHanded = 0x02000000,
TrinketOne = 0x04000000,
Cloak = 0x08000000,
SigilOne = 0x10000000, // Blue
SigilTwo = 0x20000000, // Yellow
SigilThree = 0x40000000, // Red
Clothing = 0x80000000 | HeadWear | ChestWear | AbdomenWear | UpperArmWear | LowerArmWear | HandWear | UpperLegWear | LowerLegWear | FootWear,
Armor = ChestArmor | AbdomenArmor | UpperArmArmor | LowerArmArmor | UpperLegArmor | LowerLegArmor | FootWear,
ArmorExclusive = ChestArmor | AbdomenArmor | UpperArmArmor | LowerArmArmor | UpperLegArmor | LowerLegArmor,
Extremity = HeadWear | HandWear | FootWear,
Jewelry = NeckWear | WristWearLeft | WristWearRight | FingerWearLeft | FingerWearRight | TrinketOne | Cloak | SigilOne | SigilTwo | SigilThree,
WristWear = WristWearLeft | WristWearRight,
FingerWear = FingerWearLeft | FingerWearRight,
Sigil = SigilOne | SigilTwo | SigilThree,
ReadySlot = Held | TwoHanded | TrinketOne | Cloak | SigilOne | SigilTwo,
Weapon = SigilTwo | TrinketOne | Held,
WeaponReadySlot = SigilOne | SigilTwo | TrinketOne | Held,
Selectable = MeleeWeapon | Shield | MissileWeapon | Held | TwoHanded,
SelectablePlusAmmo = Selectable | MissileAmmo,
All = 0x7FFFFFFF,
CanGoInReadySlot = 0x7FFFFFFF
}
public static class EquipMaskExtensions
{
public static int GetTotalBitsSet(this EquipMask value)
{
int slotFlags = (int)value;
int bitsSet = 0;
while (slotFlags != 0)
{
if ((slotFlags & 1) == 1)
bitsSet++;
slotFlags >>= 1;
}
return bitsSet;
}
// Some feet armor have EquipMask.Feet | EquipMask.PantsLowerLegs
public static bool IsBodyArmor(this EquipMask value)
{
return ((int)value & 0x00007F21) != 0;
}
public static bool IsCoreBodyArmor(this EquipMask value)
{
return (value & (EquipMask.ChestArmor | EquipMask.UpperArmArmor | EquipMask.LowerArmArmor | EquipMask.AbdomenArmor | EquipMask.UpperLegArmor | EquipMask.LowerLegArmor)) != 0;
}
public static bool IsExtremityBodyArmor(this EquipMask value)
{
return (value & (EquipMask.FootWear | EquipMask.HandWear | EquipMask.HeadWear)) != 0;
}
public static bool IsUnderwear(this EquipMask value)
{
if (value == (EquipMask.FootWear | EquipMask.LowerLegWear))
return false;
return ((int)value & 0x000000DE) != 0;
}
public static bool IsShirt(this EquipMask value)
{
return ((int)value & 0x0000001A) != 0;
}
public static bool IsPants(this EquipMask value)
{
if (value == (EquipMask.FootWear | EquipMask.LowerLegWear))
return false;
return ((int)value & 0x000000C4) != 0;
}
}
}

View file

@ -0,0 +1,722 @@
namespace Mag.Shared.Constants
{
// https://github.com/ACEmulator/ACE/blob/master/Source/ACE.Entity/Enum/Properties/PropertyInt.cs
public enum IntValueKey
{
// properties marked as ServerOnly are properties we never saw in PCAPs, from here:
// http://ac.yotesfan.com/ace_object/not_used_enums.php
// source: @OptimShi
// description attributes are used by the weenie editor for a cleaner display name
Undef = 0,
[ServerOnly]
ItemType = 1,
CreatureType = 2,
[ServerOnly]
PaletteTemplate = 3,
ClothingPriority = 4,
[SendOnLogin]
EncumbranceVal = 5, // ENCUMB_VAL_INT,
[SendOnLogin]
ItemsCapacity = 6,
[SendOnLogin]
ContainersCapacity = 7,
[ServerOnly]
Mass = 8,
[ServerOnly]
ValidLocations = 9, // LOCATIONS_INT
[ServerOnly]
CurrentWieldedLocation = 10,
[ServerOnly]
MaxStackSize = 11,
[ServerOnly]
StackSize = 12,
[ServerOnly]
StackUnitEncumbrance = 13,
[ServerOnly]
StackUnitMass = 14,
[ServerOnly]
StackUnitValue = 15,
[ServerOnly]
ItemUseable = 16,
RareId = 17,
[ServerOnly]
UiEffects = 18,
Value = 19,
[Ephemeral][SendOnLogin]
CoinValue = 20,
TotalExperience = 21,
AvailableCharacter = 22,
TotalSkillCredits = 23,
[SendOnLogin]
AvailableSkillCredits = 24,
[SendOnLogin]
Level = 25,
AccountRequirements = 26,
ArmorType = 27,
ArmorLevel = 28,
AllegianceCpPool = 29,
[SendOnLogin]
AllegianceRank = 30,
ChannelsAllowed = 31,
ChannelsActive = 32,
Bonded = 33,
MonarchsRank = 34,
AllegianceFollowers = 35,
ResistMagic = 36,
ResistItemAppraisal = 37,
ResistLockpick = 38,
DeprecatedResistRepair = 39,
[SendOnLogin]
CombatMode = 40,
CurrentAttackHeight = 41,
CombatCollisions = 42,
[SendOnLogin]
NumDeaths = 43,
Damage = 44,
DamageType = 45,
[ServerOnly]
DefaultCombatStyle = 46,
[SendOnLogin]
AttackType = 47,
WeaponSkill = 48,
WeaponTime = 49,
AmmoType = 50,
CombatUse = 51,
[ServerOnly]
ParentLocation = 52,
/// <summary>
/// TODO: Migrate inventory order away from this and instead use the new InventoryOrder property
/// TODO: PlacementPosition is used (very sparingly) in cache.bin, so it has (or had) a meaning at one point before we hijacked it
/// TODO: and used it for our own inventory order
/// </summary>
[ServerOnly]
PlacementPosition = 53,
WeaponEncumbrance = 54,
WeaponMass = 55,
ShieldValue = 56,
ShieldEncumbrance = 57,
MissileInventoryLocation = 58,
FullDamageType = 59,
WeaponRange = 60,
AttackersSkill = 61,
DefendersSkill = 62,
AttackersSkillValue = 63,
AttackersClass = 64,
[ServerOnly]
Placement = 65,
CheckpointStatus = 66,
Tolerance = 67,
TargetingTactic = 68,
CombatTactic = 69,
HomesickTargetingTactic = 70,
NumFollowFailures = 71,
FriendType = 72,
FoeType = 73,
MerchandiseItemTypes = 74,
MerchandiseMinValue = 75,
MerchandiseMaxValue = 76,
NumItemsSold = 77,
NumItemsBought = 78,
MoneyIncome = 79,
MoneyOutflow = 80,
[Ephemeral]
MaxGeneratedObjects = 81,
[Ephemeral]
InitGeneratedObjects = 82,
ActivationResponse = 83,
OriginalValue = 84,
NumMoveFailures = 85,
MinLevel = 86,
MaxLevel = 87,
LockpickMod = 88,
BoosterEnum = 89,
BoostValue = 90,
MaxStructure = 91,
Structure = 92,
[ServerOnly]
PhysicsState = 93,
[ServerOnly]
TargetType = 94,
RadarBlipColor = 95,
EncumbranceCapacity = 96,
LoginTimestamp = 97,
[SendOnLogin]
CreationTimestamp = 98,
PkLevelModifier = 99,
GeneratorType = 100,
AiAllowedCombatStyle = 101,
LogoffTimestamp = 102,
GeneratorDestructionType = 103,
ActivationCreateClass = 104,
ItemWorkmanship = 105,
ItemSpellcraft = 106,
ItemCurMana = 107,
ItemMaxMana = 108,
ItemDifficulty = 109,
ItemAllegianceRankLimit = 110,
PortalBitmask = 111,
AdvocateLevel = 112,
[SendOnLogin]
Gender = 113,
Attuned = 114,
ItemSkillLevelLimit = 115,
GateLogic = 116,
ItemManaCost = 117,
Logoff = 118,
Active = 119,
AttackHeight = 120,
NumAttackFailures = 121,
AiCpThreshold = 122,
AiAdvancementStrategy = 123,
Version = 124,
[SendOnLogin]
Age = 125,
VendorHappyMean = 126,
VendorHappyVariance = 127,
CloakStatus = 128,
[SendOnLogin]
VitaeCpPool = 129,
NumServicesSold = 130,
MaterialType = 131,
[SendOnLogin]
NumAllegianceBreaks = 132,
[Ephemeral]
ShowableOnRadar = 133,
[SendOnLogin]
PlayerKillerStatus = 134,
VendorHappyMaxItems = 135,
ScorePageNum = 136,
ScoreConfigNum = 137,
ScoreNumScores = 138,
[SendOnLogin]
DeathLevel = 139,
AiOptions = 140,
OpenToEveryone = 141,
GeneratorTimeType = 142,
GeneratorStartTime = 143,
GeneratorEndTime = 144,
GeneratorEndDestructionType = 145,
XpOverride = 146,
NumCrashAndTurns = 147,
ComponentWarningThreshold = 148,
HouseStatus = 149,
[ServerOnly]
HookPlacement = 150,
[ServerOnly]
HookType = 151,
[ServerOnly]
HookItemType = 152,
AiPpThreshold = 153,
GeneratorVersion = 154,
HouseType = 155,
PickupEmoteOffset = 156,
WeenieIteration = 157,
WieldRequirements = 158,
WieldSkillType = 159,
WieldDifficulty = 160,
HouseMaxHooksUsable = 161,
HouseCurrentHooksUsable = 162,
AllegianceMinLevel = 163,
AllegianceMaxLevel = 164,
HouseRelinkHookCount = 165,
SlayerCreatureType = 166,
ConfirmationInProgress = 167,
ConfirmationTypeInProgress = 168,
TsysMutationData = 169,
NumItemsInMaterial = 170,
NumTimesTinkered = 171,
AppraisalLongDescDecoration = 172,
AppraisalLockpickSuccessPercent = 173,
[Ephemeral]
AppraisalPages = 174,
[Ephemeral]
AppraisalMaxPages = 175,
AppraisalItemSkill = 176,
GemCount = 177,
GemType = 178,
ImbuedEffect = 179,
AttackersRawSkillValue = 180,
[SendOnLogin]
ChessRank = 181,
ChessTotalGames = 182,
ChessGamesWon = 183,
ChessGamesLost = 184,
TypeOfAlteration = 185,
SkillToBeAltered = 186,
SkillAlterationCount = 187,
[SendOnLogin]
HeritageGroup = 188,
TransferFromAttribute = 189,
TransferToAttribute = 190,
AttributeTransferCount = 191,
[SendOnLogin]
FakeFishingSkill = 192,
NumKeys = 193,
DeathTimestamp = 194,
PkTimestamp = 195,
VictimTimestamp = 196,
HookGroup = 197,
AllegianceSwearTimestamp = 198,
[SendOnLogin]
HousePurchaseTimestamp = 199,
RedirectableEquippedArmorCount = 200,
MeleeDefenseImbuedEffectTypeCache = 201,
MissileDefenseImbuedEffectTypeCache = 202,
MagicDefenseImbuedEffectTypeCache = 203,
ElementalDamageBonus = 204,
ImbueAttempts = 205,
ImbueSuccesses = 206,
CreatureKills = 207,
PlayerKillsPk = 208,
PlayerKillsPkl = 209,
RaresTierOne = 210,
RaresTierTwo = 211,
RaresTierThree = 212,
RaresTierFour = 213,
RaresTierFive = 214,
[SendOnLogin]
AugmentationStat = 215,
[SendOnLogin]
AugmentationFamilyStat = 216,
[SendOnLogin]
AugmentationInnateFamily = 217,
[SendOnLogin]
AugmentationInnateStrength = 218,
[SendOnLogin]
AugmentationInnateEndurance = 219,
[SendOnLogin]
AugmentationInnateCoordination = 220,
[SendOnLogin]
AugmentationInnateQuickness = 221,
[SendOnLogin]
AugmentationInnateFocus = 222,
[SendOnLogin]
AugmentationInnateSelf = 223,
[SendOnLogin]
AugmentationSpecializeSalvaging = 224,
[SendOnLogin]
AugmentationSpecializeItemTinkering = 225,
[SendOnLogin]
AugmentationSpecializeArmorTinkering = 226,
[SendOnLogin]
AugmentationSpecializeMagicItemTinkering = 227,
[SendOnLogin]
AugmentationSpecializeWeaponTinkering = 228,
[SendOnLogin]
AugmentationExtraPackSlot = 229,
[SendOnLogin]
AugmentationIncreasedCarryingCapacity = 230,
[SendOnLogin]
AugmentationLessDeathItemLoss = 231,
[SendOnLogin]
AugmentationSpellsRemainPastDeath = 232,
[SendOnLogin]
AugmentationCriticalDefense = 233,
[SendOnLogin]
AugmentationBonusXp = 234,
[SendOnLogin]
AugmentationBonusSalvage = 235,
[SendOnLogin]
AugmentationBonusImbueChance = 236,
[SendOnLogin]
AugmentationFasterRegen = 237,
[SendOnLogin]
AugmentationIncreasedSpellDuration = 238,
[SendOnLogin]
AugmentationResistanceFamily = 239,
[SendOnLogin]
AugmentationResistanceSlash = 240,
[SendOnLogin]
AugmentationResistancePierce = 241,
[SendOnLogin]
AugmentationResistanceBlunt = 242,
[SendOnLogin]
AugmentationResistanceAcid = 243,
[SendOnLogin]
AugmentationResistanceFire = 244,
[SendOnLogin]
AugmentationResistanceFrost = 245,
[SendOnLogin]
AugmentationResistanceLightning = 246,
RaresTierOneLogin = 247,
RaresTierTwoLogin = 248,
RaresTierThreeLogin = 249,
RaresTierFourLogin = 250,
RaresTierFiveLogin = 251,
RaresLoginTimestamp = 252,
RaresTierSix = 253,
RaresTierSeven = 254,
RaresTierSixLogin = 255,
RaresTierSevenLogin = 256,
ItemAttributeLimit = 257,
ItemAttributeLevelLimit = 258,
ItemAttribute2ndLimit = 259,
ItemAttribute2ndLevelLimit = 260,
CharacterTitleId = 261,
NumCharacterTitles = 262,
ResistanceModifierType = 263,
FreeTinkersBitfield = 264,
EquipmentSetId = 265,
PetClass = 266,
Lifespan = 267,
[Ephemeral]
RemainingLifespan = 268,
UseCreateQuantity = 269,
WieldRequirements2 = 270,
WieldSkillType2 = 271,
WieldDifficulty2 = 272,
WieldRequirements3 = 273,
WieldSkillType3 = 274,
WieldDifficulty3 = 275,
WieldRequirements4 = 276,
WieldSkillType4 = 277,
WieldDifficulty4 = 278,
Unique = 279,
SharedCooldown = 280,
Faction1Bits = 281,
Faction2Bits = 282,
Faction3Bits = 283,
Hatred1Bits = 284,
Hatred2Bits = 285,
Hatred3Bits = 286,
SocietyRankCelhan = 287,
SocietyRankEldweb = 288,
SocietyRankRadblo = 289,
HearLocalSignals = 290,
HearLocalSignalsRadius = 291,
Cleaving = 292,
[SendOnLogin]
AugmentationSpecializeGearcraft = 293,
[SendOnLogin]
AugmentationInfusedCreatureMagic = 294,
[SendOnLogin]
AugmentationInfusedItemMagic = 295,
[SendOnLogin]
AugmentationInfusedLifeMagic = 296,
[SendOnLogin]
AugmentationInfusedWarMagic = 297,
[SendOnLogin]
AugmentationCriticalExpertise = 298,
[SendOnLogin]
AugmentationCriticalPower = 299,
[SendOnLogin]
AugmentationSkilledMelee = 300,
[SendOnLogin]
AugmentationSkilledMissile = 301,
[SendOnLogin]
AugmentationSkilledMagic = 302,
ImbuedEffect2 = 303,
ImbuedEffect3 = 304,
ImbuedEffect4 = 305,
ImbuedEffect5 = 306,
[SendOnLogin]
DamageRating = 307,
[SendOnLogin]
DamageResistRating = 308,
[SendOnLogin]
AugmentationDamageBonus = 309,
[SendOnLogin]
AugmentationDamageReduction = 310,
ImbueStackingBits = 311,
[SendOnLogin]
HealOverTime = 312,
[SendOnLogin]
CritRating = 313,
[SendOnLogin]
CritDamageRating = 314,
[SendOnLogin]
CritResistRating = 315,
[SendOnLogin]
CritDamageResistRating = 316,
[SendOnLogin]
HealingResistRating = 317,
[SendOnLogin]
DamageOverTime = 318,
ItemMaxLevel = 319,
ItemXpStyle = 320,
EquipmentSetExtra = 321,
[SendOnLogin]
AetheriaBitfield = 322,
[SendOnLogin]
HealingBoostRating = 323,
HeritageSpecificArmor = 324,
AlternateRacialSkills = 325,
[SendOnLogin]
AugmentationJackOfAllTrades = 326,
[SendOnLogin]
AugmentationResistanceNether = 327,
[SendOnLogin]
AugmentationInfusedVoidMagic = 328,
[SendOnLogin]
WeaknessRating = 329,
[SendOnLogin]
NetherOverTime = 330,
[SendOnLogin]
NetherResistRating = 331,
LuminanceAward = 332,
[SendOnLogin]
LumAugDamageRating = 333,
[SendOnLogin]
LumAugDamageReductionRating = 334,
[SendOnLogin]
LumAugCritDamageRating = 335,
[SendOnLogin]
LumAugCritReductionRating = 336,
[SendOnLogin]
LumAugSurgeEffectRating = 337,
[SendOnLogin]
LumAugSurgeChanceRating = 338,
[SendOnLogin]
LumAugItemManaUsage = 339,
[SendOnLogin]
LumAugItemManaGain = 340,
[SendOnLogin]
LumAugVitality = 341,
[SendOnLogin]
LumAugHealingRating = 342,
[SendOnLogin]
LumAugSkilledCraft = 343,
[SendOnLogin]
LumAugSkilledSpec = 344,
[SendOnLogin]
LumAugNoDestroyCraft = 345,
RestrictInteraction = 346,
OlthoiLootTimestamp = 347,
OlthoiLootStep = 348,
UseCreatesContractId = 349,
[SendOnLogin]
DotResistRating = 350,
[SendOnLogin]
LifeResistRating = 351,
CloakWeaveProc = 352,
WeaponType = 353,
[SendOnLogin]
MeleeMastery = 354,
[SendOnLogin]
RangedMastery = 355,
SneakAttackRating = 356,
RecklessnessRating = 357,
DeceptionRating = 358,
CombatPetRange = 359,
[SendOnLogin]
WeaponAuraDamage = 360,
[SendOnLogin]
WeaponAuraSpeed = 361,
[SendOnLogin]
SummoningMastery = 362,
HeartbeatLifespan = 363,
UseLevelRequirement = 364,
[SendOnLogin]
LumAugAllSkills = 365,
UseRequiresSkill = 366,
UseRequiresSkillLevel = 367,
UseRequiresSkillSpec = 368,
UseRequiresLevel = 369,
[SendOnLogin]
GearDamage = 370,
[SendOnLogin]
GearDamageResist = 371,
[SendOnLogin]
GearCrit = 372,
[SendOnLogin]
GearCritResist = 373,
[SendOnLogin]
GearCritDamage = 374,
[SendOnLogin]
GearCritDamageResist = 375,
[SendOnLogin]
GearHealingBoost = 376,
[SendOnLogin]
GearNetherResist = 377,
[SendOnLogin]
GearLifeResist = 378,
[SendOnLogin]
GearMaxHealth = 379,
Unknown380 = 380,
[SendOnLogin]
PKDamageRating = 381,
[SendOnLogin]
PKDamageResistRating = 382,
[SendOnLogin]
GearPKDamageRating = 383,
[SendOnLogin]
GearPKDamageResistRating = 384,
Unknown385 = 385,
/// <summary>
/// Overpower chance % for endgame creatures.
/// </summary>
[SendOnLogin]
Overpower = 386,
[SendOnLogin]
OverpowerResist = 387,
// Client does not display accurately
[SendOnLogin]
GearOverpower = 388,
// Client does not display accurately
[SendOnLogin]
GearOverpowerResist = 389,
// Number of times a character has enlightened
[SendOnLogin]
Enlightenment = 390,
// ACE Specific
[ServerOnly]
PCAPRecordedAutonomousMovement = 8007,
[ServerOnly]
PCAPRecordedMaxVelocityEstimated = 8030,
[ServerOnly]
PCAPRecordedPlacement = 8041,
[ServerOnly]
PCAPRecordedAppraisalPages = 8042,
[ServerOnly]
PCAPRecordedAppraisalMaxPages = 8043,
//[ServerOnly]
//TotalLogins = 9001,
//[ServerOnly]
//DeletionTimestamp = 9002,
//[ServerOnly]
//CharacterOptions1 = 9003,
//[ServerOnly]
//CharacterOptions2 = 9004,
//[ServerOnly]
//LootTier = 9005,
//[ServerOnly]
//GeneratorProbability = 9006,
//[ServerOnly]
//WeenieType = 9007 // I don't think this property type is needed anymore. We don't store the weenie type in the property bags, we store it as a separate field in the base objects.
[ServerOnly]
CurrentLoyaltyAtLastLogoff = 9008,
[ServerOnly]
CurrentLeadershipAtLastLogoff = 9009,
[ServerOnly]
AllegianceOfficerRank = 9010,
[ServerOnly]
HouseRentTimestamp = 9011,
/// <summary>
/// Stores the player's selected hairstyle at creation or after a barber use. This is used only for Gear Knights and Olthoi characters who have more than a single part/texture for a "hairstyle" (BodyStyle)
/// </summary>
[ServerOnly]
Hairstyle = 9012,
/// <summary>
/// Used to store the calculated Clothing Priority for use with armor reduced items and items like Over-Robes.
/// </summary>
[Ephemeral][ServerOnly]
VisualClothingPriority = 9013,
[ServerOnly]
SquelchGlobal = 9014,
/// <summary>
/// TODO: This is a place holder for future use. See PlacementPosition
/// This is the sort order for items in a container
/// </summary>
[ServerOnly]
InventoryOrder = 9015,
// Decal Specific
WeenieClassId_Decal = 218103808,
Icon_Decal_DID = 218103809,
Container_Decal_IID = 218103810,
Landblock_Decal = 218103811,
ItemSlots_Decal = 218103812,
PackSlots_Decal = 218103813,
StackCount_Decal = 218103814,
StackMax_Decal = 218103815,
Spell_Decal_DID = 218103816,
SlotLegacy_Decal = 218103817,
Wielder_Decal_IID = 218103818,
WieldingSlot_Decal = 218103819,
Monarch_Decal_IID = 218103820,
Coverage_Decal = 218103821,
EquipableSlots_Decal = 218103822,
EquipType_Decal = 218103823,
IconOutline_Decal = 218103824,
MissileType_Decal = 218103825,
UsageMask_Decal = 218103826,
HouseOwner_Decal_IID = 218103827,
HookMask_Decal = 218103828,
HookType_Decal = 218103829,
Setup_Decal_DID = 218103830,
ObjectDescriptionFlags_Decal = 218103831,
CreateFlags1_Decal = 218103832,
CreateFlags2_Decal = 218103833,
Category_Decal = 218103834,
Behavior_Decal = 218103835,
MagicDef_Decal = 218103836,
SpecialProps_Decal = 218103837,
SpellCount_Decal = 218103838,
WeapSpeed_Decal = 218103839,
EquipSkill_Decal = 218103840,
DamageType_Decal = 218103841,
MaxDamage_Decal = 218103842,
Unknown10_Decal = 218103843, // CurrentWieldLocation?
Unknown100000_Decal = 218103844, // RadarBlipColor ???
Unknown800000_Decal = 218103845,
Unknown8000000_Decal = 218103846,
PhysicsDataFlags_Decal = 218103847,
ActiveSpellCount_Decal = 218103848,
IconOverlay_Decal_DID = 218103849,
IconUnderlay_Decal_DID = 218103850,
Slot_Decal = 231735296,
}
public static class IntValueKeyTools
{
/// <summary>
/// Converts a decal specific IntValueKey to the actual IntValueKey.
/// If this is not an IntValueKey, 0 will be returned.
/// </summary>
public static uint ConvertToInt(IntValueKey input)
{
if (input == IntValueKey.Category_Decal) return (int)IntValueKey.ItemType;
if (input == IntValueKey.Coverage_Decal) return (int)IntValueKey.ClothingPriority;
if (input == IntValueKey.ItemSlots_Decal) return (int)IntValueKey.ItemsCapacity;
if (input == IntValueKey.PackSlots_Decal) return (int)IntValueKey.ContainersCapacity;
if (input == IntValueKey.EquipableSlots_Decal) return (int)IntValueKey.ValidLocations;
//if (input == IntValueKey.WieldingSlot_Decal) return (int)IntValueKey.CurrentWieldedLocation;
if (input == IntValueKey.StackMax_Decal) return (int)IntValueKey.MaxStackSize;
if (input == IntValueKey.StackCount_Decal) return (int)IntValueKey.StackSize;
if (input == IntValueKey.IconOutline_Decal) return (int)IntValueKey.UiEffects;
if (input == IntValueKey.MaxDamage_Decal) return (int)IntValueKey.Damage;
if (input == IntValueKey.DamageType_Decal) return (int)IntValueKey.DamageType;
if (input == IntValueKey.EquipSkill_Decal) return (int)IntValueKey.WeaponSkill;
if (input == IntValueKey.WeapSpeed_Decal) return (int)IntValueKey.WeaponTime;
if (input == IntValueKey.MissileType_Decal) return (int)IntValueKey.AmmoType;
if (input == IntValueKey.EquipType_Decal) return (int)IntValueKey.CombatUse;
if (input == IntValueKey.UsageMask_Decal) return (int)IntValueKey.TargetType;
if (input == IntValueKey.HookMask_Decal) return (int)IntValueKey.HookType;
return 0;
}
/// <summary>
/// If input is not a IID, 0 will be returned
/// </summary>
public static uint ConvertToIID(IntValueKey input)
{
if (input == IntValueKey.Container_Decal_IID) return 2; // CONTAINER_IID
if (input == IntValueKey.Wielder_Decal_IID) return 3; // WIELDER_IID
if (input == IntValueKey.Monarch_Decal_IID) return 26; // MONARCH_IID
if (input == IntValueKey.HouseOwner_Decal_IID) return 32; // HOUSE_OWNER_IID
return 0;
}
/// <summary>
/// If input is not a DID, 0 will be returned
/// </summary>
public static uint ConvertToDID(IntValueKey input)
{
if (input == IntValueKey.Setup_Decal_DID) return 1; // SETUP_DID
if (input == IntValueKey.Icon_Decal_DID) return 8; // ICON_DID
if (input == IntValueKey.Spell_Decal_DID) return 28; // SPELL_DID
if (input == IntValueKey.IconOverlay_Decal_DID) return 50; // ICON_OVERLAY_DID
if (input == IntValueKey.IconUnderlay_Decal_DID) return 52; // ICON_UNDERLAY_DID
return 0;
}
}
}

View file

@ -0,0 +1,54 @@
using System;
namespace Mag.Shared.Constants
{
[Flags]
public enum ItemType : uint
{
None = 0x00000000,
MeleeWeapon = 0x00000001,
Armor = 0x00000002,
Clothing = 0x00000004,
Jewelry = 0x00000008,
Creature = 0x00000010,
Food = 0x00000020,
Money = 0x00000040,
Misc = 0x00000080,
MissileWeapon = 0x00000100,
Container = 0x00000200,
Useless = 0x00000400,
Gem = 0x00000800,
SpellComponents = 0x00001000,
Writable = 0x00002000,
Key = 0x00004000,
Caster = 0x00008000,
Portal = 0x00010000,
Lockable = 0x00020000,
PromissoryNote = 0x00040000,
ManaStone = 0x00080000,
Service = 0x00100000,
MagicWieldable = 0x00200000,
CraftCookingBase = 0x00400000,
CraftAlchemyBase = 0x00800000,
CraftFletchingBase = 0x02000000,
CraftAlchemyIntermediate = 0x04000000,
CraftFletchingIntermediate = 0x08000000,
LifeStone = 0x10000000,
TinkeringTool = 0x20000000,
TinkeringMaterial = 0x40000000,
Gameboard = 0x80000000,
PortalMagicTarget = Portal | LifeStone,
LockableMagicTarget = Misc | Container,
Vestements = Armor | Clothing,
Weapon = MeleeWeapon | MissileWeapon,
WeaponOrCaster = MeleeWeapon | MissileWeapon | Caster,
Item = MeleeWeapon | Armor | Clothing | Jewelry | Food | Money | Misc | MissileWeapon | Container |
Gem | SpellComponents | Writable | Key | Caster | Portal | PromissoryNote | ManaStone | MagicWieldable,
RedirectableItemEnchantmentTarget = MeleeWeapon | Armor | Clothing | MissileWeapon | Caster,
ItemEnchantableTarget = MeleeWeapon | Armor | Clothing | Jewelry | Misc | MissileWeapon | Container | Gem | Caster | ManaStone,
VendorShopKeep = MeleeWeapon | Armor | Clothing | Food | Misc | MissileWeapon | Container | Useless | Writable | Key |
PromissoryNote | CraftFletchingIntermediate | TinkeringMaterial,
VendorGrocer = Food | Container | Writable | Key | PromissoryNote | CraftCookingBase
}
}

View file

@ -0,0 +1,80 @@

namespace Mag.Shared.Constants
{
public enum MaterialType : uint
{
Unknown = 0x00000000,
Ceramic = 0x00000001,
Porcelain = 0x00000002,
Linen = 0x00000004,
Satin = 0x00000005,
Silk = 0x00000006,
Velvet = 0x00000007,
Wool = 0x00000008,
Agate = 0x0000000A,
Amber = 0x0000000B,
Amethyst = 0x0000000C,
Aquamarine = 0x0000000D,
Azurite = 0x0000000E,
BlackGarnet = 0x0000000F,
BlackOpal = 0x00000010,
Bloodstone = 0x00000011,
Carnelian = 0x00000012,
Citrine = 0x00000013,
Diamond = 0x00000014,
Emerald = 0x00000015,
FireOpal = 0x00000016,
GreenGarnet = 0x00000017,
GreenJade = 0x00000018,
Hematite = 0x00000019,
ImperialTopaz = 0x0000001A,
Jet = 0x0000001B,
LapisLazuli = 0x0000001C,
LavenderJade = 0x0000001D,
Malachite = 0x0000001E,
Moonstone = 0x0000001F,
Onyx = 0x00000020,
Opal = 0x00000021,
Peridot = 0x00000022,
RedGarnet = 0x00000023,
RedJade = 0x00000024,
RoseQuartz = 0x00000025,
Ruby = 0x00000026,
Sapphire = 0x00000027,
SmokeyQuartz = 0x00000028,
Sunstone = 0x00000029,
TigerEye = 0x0000002A,
Tourmaline = 0x0000002B,
Turquoise = 0x0000002C,
WhiteJade = 0x0000002D,
WhiteQuartz = 0x0000002E,
WhiteSapphire = 0x0000002F,
YellowGarnet = 0x00000030,
YellowTopaz = 0x00000031,
Zircon = 0x00000032,
Ivory = 0x00000033,
Leather = 0x00000034,
ArmoredilloHide = 0x00000035,
GromnieHide = 0x00000036,
ReedSharkHide = 0x00000037,
Brass = 0x00000039,
Bronze = 0x0000003A,
Copper = 0x0000003B,
Gold = 0x0000003C,
Iron = 0x0000003D,
Pyreal = 0x0000003E,
Silver = 0x0000003F,
Steel = 0x00000040,
Alabaster = 0x00000042,
Granite = 0x00000043,
Marble = 0x00000044,
Obsidian = 0x00000045,
Sandstone = 0x00000046,
Serpentine = 0x00000047,
Ebony = 0x00000049,
Mahogany = 0x0000004A,
Oak = 0x0000004B,
Pine = 0x0000004C,
Teak = 0x0000004D,
}
}

View file

@ -0,0 +1,33 @@
namespace Mag.Shared.Constants
{
// https://github.com/ACEmulator/ACE/blob/master/Source/ACE.Entity/Enum/Properties/PropertyInt64.cs
public enum QuadValueKey
{
Undef = 0,
[SendOnLogin]
TotalExperience = 1,
[SendOnLogin]
AvailableExperience = 2,
AugmentationCost = 3,
ItemTotalXp = 4,
ItemBaseXp = 5,
[SendOnLogin]
AvailableLuminance = 6,
[SendOnLogin]
MaximumLuminance = 7,
InteractionReqs = 8,
// ACE Specific
/* custom */
[ServerOnly]
AllegianceXPCached = 9000,
[ServerOnly]
AllegianceXPGenerated = 9001,
[ServerOnly]
AllegianceXPReceived = 9002,
[ServerOnly]
VerifyXp = 9003
}
}

View file

@ -0,0 +1,11 @@
using System;
namespace Mag.Shared.Constants
{
/// <summary>
/// These are properties that aren't saved to the shard.
/// </summary>
public class SendOnLoginAttribute : Attribute
{
}
}

View file

@ -0,0 +1,11 @@
using System;
namespace Mag.Shared.Constants
{
/// <summary>
/// These are properties that aren't saved to the shard.
/// </summary>
public class ServerOnlyAttribute : Attribute
{
}
}

67
Shared/Constants/Skill.cs Normal file
View file

@ -0,0 +1,67 @@
namespace Mag.Shared.Constants
{
/// <summary>
/// note: even though these are unnumbered, order is very important. values of "none" or commented
/// as retired or unused --ABSOLUTELY CANNOT-- be removed. Skills that are none, retired, or not
/// implemented have been removed from the SkillHelper.ValidSkills hashset below.
/// </summary>
public enum Skill
{
None,
Axe, /* Retired */
Bow, /* Retired */
Crossbow, /* Retired */
Dagger, /* Retired */
Mace, /* Retired */
MeleeDefense,
MissileDefense,
Sling, /* Retired */
Spear, /* Retired */
Staff, /* Retired */
Sword, /* Retired */
ThrownWeapon, /* Retired */
UnarmedCombat, /* Retired */
ArcaneLore,
MagicDefense,
ManaConversion,
Spellcraft, /* Unimplemented */
ItemTinkering,
AssessPerson,
Deception,
Healing,
Jump,
Lockpick,
Run,
Awareness, /* Unimplemented */
ArmsAndArmorRepair, /* Unimplemented */
AssessCreature,
WeaponTinkering,
ArmorTinkering,
MagicItemTinkering,
CreatureEnchantment,
ItemEnchantment,
LifeMagic,
WarMagic,
Leadership,
Loyalty,
Fletching,
Alchemy,
Cooking,
Salvaging,
TwoHandedCombat,
Gearcraft, /* Retired */
VoidMagic,
HeavyWeapons,
LightWeapons,
FinesseWeapons,
MissileWeapons,
Shield,
DualWield,
Recklessness,
SneakAttack,
DirtyFighting,
Challenge, /* Unimplemented */
Summoning
}
}

View file

@ -0,0 +1,707 @@
namespace Mag.Shared.Constants
{
public enum SpellCategory
{
Undefined,
Strength_Raising,
Strength_Lowering,
Endurance_Raising,
Endurance_Lowering,
Quickness_Raising,
Quickness_Lowering,
Coordination_Raising,
Coordination_Lowering,
Focus_Raising,
Focus_Lowering,
Self_Raising,
Self_Lowering,
Focus_Concentration,
Focus_Disruption,
Focus_Brilliance,
Focus_Dullness,
Axe_Raising,
Axe_Lowering,
Bow_Raising,
Bow_Lowering,
Crossbow_Raising,
Crossbow_Lowering,
Dagger_Raising,
Dagger_Lowering,
Mace_Raising,
Mace_Lowering,
Spear_Raising,
Spear_Lowering,
Staff_Raising,
Staff_Lowering,
Sword_Raising,
Sword_Lowering,
Thrown_Weapons_Raising,
Thrown_Weapons_Lowering,
Unarmed_Combat_Raising,
Unarmed_Combat_Lowering,
Melee_Defense_Raising,
Melee_Defense_Lowering,
Missile_Defense_Raising,
Missile_Defense_Lowering,
Magic_Defense_Raising,
Magic_Defense_Lowering,
Creature_Enchantment_Raising,
Creature_Enchantment_Lowering,
Item_Enchantment_Raising,
Item_Enchantment_Lowering,
Life_Magic_Raising,
Life_Magic_Lowering,
War_Magic_Raising,
War_Magic_Lowering,
Mana_Conversion_Raising,
Mana_Conversion_Lowering,
Arcane_Lore_Raising,
Arcane_Lore_Lowering,
Appraise_Armor_Raising,
Appraise_Armor_Lowering,
Appraise_Item_Raising,
Appraise_Item_Lowering,
Appraise_Magic_Item_Raising,
Appraise_Magic_Item_Lowering,
Appraise_Weapon_Raising,
Appraise_Weapon_Lowering,
Assess_Monster_Raising,
Assess_Monster_Lowering,
Deception_Raising,
Deception_Lowering,
Healing_Raising,
Healing_Lowering,
Jump_Raising,
Jump_Lowering,
Leadership_Raising,
Leadership_Lowering,
Lockpick_Raising,
Lockpick_Lowering,
Loyalty_Raising,
Loyalty_Lowering,
Run_Raising,
Run_Lowering,
Health_Raising,
Health_Lowering,
Stamina_Raising,
Stamina_Lowering,
Mana_Raising,
Mana_Lowering,
Mana_Remedy,
Mana_Malediction,
Health_Transfer_to_caster,
Health_Transfer_from_caster,
Stamina_Transfer_to_caster,
Stamina_Transfer_from_caster,
Mana_Transfer_to_caster,
Mana_Transfer_from_caster,
Health_Accelerating,
Health_Decelerating,
Stamina_Accelerating,
Stamina_Decelerating,
Mana_Accelerating,
Mana_Decelerating,
Vitae_Raising,
Vitae_Lowering,
Acid_Protection,
Acid_Vulnerability,
Bludgeon_Protection,
Bludgeon_Vulnerability,
Cold_Protection,
Cold_Vulnerability,
Electric_Protection,
Electric_Vulnerability,
Fire_Protection,
Fire_Vulnerability,
Pierce_Protection,
Pierce_Vulnerability,
Slash_Protection,
Slash_Vulnerability,
Armor_Raising,
Armor_Lowering,
Acid_Missile,
Bludgeoning_Missile,
Cold_Missile,
Electric_Missile,
Fire_Missile,
Piercing_Missile,
Slashing_Missile,
Acid_Seeker,
Bludgeoning_Seeker,
Cold_Seeker,
Electric_Seeker,
Fire_Seeker,
Piercing_Seeker,
Slashing_Seeker,
Acid_Burst,
Bludgeoning_Burst,
Cold_Burst,
Electric_Burst,
Fire_Burst,
Piercing_Burst,
Slashing_Burst,
Acid_Blast,
Bludgeoning_Blast,
Cold_Blast,
Electric_Blast,
Fire_Blast,
Piercing_Blast,
Slashing_Blast,
Acid_Scatter,
Bludgeoning_Scatter,
Cold_Scatter,
Electric_Scatter,
Fire_Scatter,
Piercing_Scatter,
Slashing_Scatter,
Attack_Mod_Raising,
Attack_Mod_Lowering,
Damage_Raising,
Damage_Lowering,
Defense_Mod_Raising,
Defense_Mod_Lowering,
Weapon_Time_Raising,
Weapon_Time_Lowering,
Armor_Value_Raising,
Armor_Value_Lowering,
Acid_Resistance_Raising,
Acid_Resistance_Lowering,
Bludgeon_Resistance_Raising,
Bludgeon_Resistance_Lowering,
Cold_Resistance_Raising,
Cold_Resistance_Lowering,
Electric_Resistance_Raising,
Electric_Resistance_Lowering,
Fire_Resistance_Raising,
Fire_Resistance_Lowering,
Pierce_Resistance_Raising,
Pierce_Resistance_Lowering,
Slash_Resistance_Raising,
Slash_Resistance_Lowering,
Bludgeoning_Resistance_Raising,
Bludgeoning_Resistance_Lowering,
Slashing_Resistance_Raising,
Slashing_Resistance_Lowering,
Piercing_Resistance_Raising,
Piercing_Resistance_Lowering,
Electrical_Resistance_Raising,
Electrical_Resistance_Lowering,
Frost_Resistance_Raising,
Frost_Resistance_Lowering,
Flame_Resistance_Raising,
Flame_Resistance_Lowering,
Acidic_Resistance_Raising,
Acidic_Resistance_Lowering,
Armor_Level_Raising,
Armor_Level_Lowering,
Lockpick_Resistance_Raising,
Lockpick_Resistance_Lowering,
Appraisal_Resistance_Raising,
Appraisal_Resistance_Lowering,
Vision_Raising,
Vision_Lowering,
Transparency_Raising,
Transparency_Lowering,
Portal_Tie,
Portal_Recall,
Portal_Creation,
Portal_Item_Creation,
Vitae,
Assess_Person_Raising,
Assess_Person_Lowering,
Acid_Volley,
Bludgeoning_Volley,
Frost_Volley,
Lightning_Volley,
Flame_Volley,
Force_Volley,
Blade_Volley,
Portal_Sending,
Lifestone_Sending,
Cooking_Raising,
Cooking_Lowering,
Fletching_Raising,
Fletching_Lowering,
Alchemy_Lowering,
Alchemy_Raising,
Acid_Ring,
Bludgeoning_Ring,
Cold_Ring,
Electric_Ring,
Fire_Ring,
Piercing_Ring,
Slashing_Ring,
Acid_Wall,
Bludgeoning_Wall,
Cold_Wall,
Electric_Wall,
Fire_Wall,
Piercing_Wall,
Slashing_Wall,
Acid_Strike,
Bludgeoning_Strike,
Cold_Strike,
Electric_Strike,
Fire_Strike,
Piercing_Strike,
Slashing_Strike,
Acid_Streak,
Bludgeoning_Streak,
Cold_Streak,
Electric_Streak,
Fire_Streak,
Piercing_Streak,
Slashing_Streak,
Dispel,
Creature_Mystic_Raising,
Creature_Mystic_Lowering,
Item_Mystic_Raising,
Item_Mystic_Lowering,
War_Mystic_Raising,
War_Mystic_Lowering,
Health_Restoring,
Health_Depleting,
Mana_Restoring,
Mana_Depleting,
Strength_Increase,
Strength_Decrease,
Endurance_Increase,
Endurance_Decrease,
Quickness_Increase,
Quickness_Decrease,
Coordination_Increase,
Coordination_Decrease,
Focus_Increase,
Focus_Decrease,
Self_Increase,
Self_Decrease,
GreatVitality_Raising,
PoorVitality_Lowering,
GreatVigor_Raising,
PoorVigor_Lowering,
GreaterIntellect_Raising,
LessorIntellect_Lowering,
LifeGiver_Raising,
LifeTaker_Lowering,
StaminaGiver_Raising,
StaminaTaker_Lowering,
ManaGiver_Raising,
ManaTaker_Lowering,
Acid_Ward_Protection,
Acid_Ward_Vulnerability,
Fire_Ward_Protection,
Fire_Ward_Vulnerability,
Cold_Ward_Protection,
Cold_Ward_Vulnerability,
Electric_Ward_Protection,
Electric_Ward_Vulnerability,
Leadership_Obedience_Raising,
Leadership_Obedience_Lowering,
Melee_Defense_Shelter_Raising,
Melee_Defense_Shelter_Lowering,
Missile_Defense_Shelter_Raising,
Missile_Defense_Shelter_Lowering,
Magic_Defense_Shelter_Raising,
Magic_Defense_Shelter_Lowering,
HuntersAcumen_Raising,
HuntersAcumen_Lowering,
StillWater_Raising,
StillWater_Lowering,
StrengthofEarth_Raising,
StrengthofEarth_Lowering,
Torrent_Raising,
Torrent_Lowering,
Growth_Raising,
Growth_Lowering,
CascadeAxe_Raising,
CascadeAxe_Lowering,
CascadeDagger_Raising,
CascadeDagger_Lowering,
CascadeMace_Raising,
CascadeMace_Lowering,
CascadeSpear_Raising,
CascadeSpear_Lowering,
CascadeStaff_Raising,
CascadeStaff_Lowering,
StoneCliffs_Raising,
StoneCliffs_Lowering,
MaxDamage_Raising,
MaxDamage_Lowering,
Bow_Damage_Raising,
Bow_Damage_Lowering,
Bow_Range_Raising,
Bow_Range_Lowering,
Extra_Defense_Mod_Raising,
Extra_Defense_Mod_Lowering,
Extra_Bow_Skill_Raising,
Extra_Bow_Skill_Lowering,
Extra_Alchemy_Skill_Raising,
Extra_Alchemy_Skill_Lowering,
Extra_Arcane_Lore_Skill_Raising,
Extra_Arcane_Lore_Skill_Lowering,
Extra_Appraise_Armor_Skill_Raising,
Extra_Appraise_Armor_Skill_Lowering,
Extra_Cooking_Skill_Raising,
Extra_Cooking_Skill_Lowering,
Extra_Crossbow_Skill_Raising,
Extra_Crossbow_Skill_Lowering,
Extra_Deception_Skill_Raising,
Extra_Deception_Skill_Lowering,
Extra_Loyalty_Skill_Raising,
Extra_Loyalty_Skill_Lowering,
Extra_Fletching_Skill_Raising,
Extra_Fletching_Skill_Lowering,
Extra_Healing_Skill_Raising,
Extra_Healing_Skill_Lowering,
Extra_Melee_Defense_Skill_Raising,
Extra_Melee_Defense_Skill_Lowering,
Extra_Appraise_Item_Skill_Raising,
Extra_Appraise_Item_Skill_Lowering,
Extra_Jumping_Skill_Raising,
Extra_Jumping_Skill_Lowering,
Extra_Life_Magic_Skill_Raising,
Extra_Life_Magic_Skill_Lowering,
Extra_Lockpick_Skill_Raising,
Extra_Lockpick_Skill_Lowering,
Extra_Appraise_Magic_Item_Skill_Raising,
Extra_Appraise_Magic_Item_Skill_Lowering,
Extra_Mana_Conversion_Skill_Raising,
Extra_Mana_Conversion_Skill_Lowering,
Extra_Assess_Creature_Skill_Raising,
Extra_Assess_Creature_Skill_Lowering,
Extra_Assess_Person_Skill_Raising,
Extra_Assess_Person_Skill_Lowering,
Extra_Run_Skill_Raising,
Extra_Run_Skill_Lowering,
Extra_Sword_Skill_Raising,
Extra_Sword_Skill_Lowering,
Extra_Thrown_Weapons_Skill_Raising,
Extra_Thrown_Weapons_Skill_Lowering,
Extra_Unarmed_Combat_Skill_Raising,
Extra_Unarmed_Combat_Skill_Lowering,
Extra_Appraise_Weapon_Skill_Raising,
Extra_Appraise_Weapon_Skill_Lowering,
Armor_Increase,
Armor_Decrease,
Extra_Acid_Resistance_Raising,
Extra_Acid_Resistance_Lowering,
Extra_Bludgeon_Resistance_Raising,
Extra_Bludgeon_Resistance_Lowering,
Extra_Fire_Resistance_Raising,
Extra_Fire_Resistance_Lowering,
Extra_Cold_Resistance_Raising,
Extra_Cold_Resistance_Lowering,
Extra_Attack_Mod_Raising,
Extra_Attack_Mod_Lowering,
Extra_Armor_Value_Raising,
Extra_Armor_Value_Lowering,
Extra_Pierce_Resistance_Raising,
Extra_Pierce_Resistance_Lowering,
Extra_Slash_Resistance_Raising,
Extra_Slash_Resistance_Lowering,
Extra_Electric_Resistance_Raising,
Extra_Electric_Resistance_Lowering,
Extra_Weapon_Time_Raising,
Extra_Weapon_Time_Lowering,
Bludgeon_Ward_Protection,
Bludgeon_Ward_Vulnerability,
Slash_Ward_Protection,
Slash_Ward_Vulnerability,
Pierce_Ward_Protection,
Pierce_Ward_Vulnerability,
Stamina_Restoring,
Stamina_Depleting,
Fireworks,
Health_Divide,
Stamina_Divide,
Mana_Divide,
Coordination_Increase2,
Strength_Increase2,
Focus_Increase2,
Endurance_Increase2,
Self_Increase2,
Melee_Defense_Multiply,
Missile_Defense_Multiply,
Magic_Defense_Multiply,
Attributes_Decrease,
LifeGiver_Raising2,
Item_Enchantment_Raising2,
Skills_Decrease,
Extra_Mana_Conversion_Bonus,
War_Mystic_Raising2,
War_Mystic_Lowering2,
Magic_Defense_Shelter_Raising2,
Extra_Life_Magic_Skill_Raising2,
Creature_Mystic_Raising2,
Item_Mystic_Raising2,
Mana_Raising2,
Self_Raising2,
CreatureEnchantment_Raising2,
Salvaging_Raising,
Extra_Salvaging_Raising,
Extra_Salvaging_Raising2,
CascadeAxe_Raising2,
Extra_Bow_Skill_Raising2,
Extra_Thrown_Weapons_Skill_Raising2,
Extra_Crossbow_Skill_Raising2,
CascadeDagger_Raising2,
CascadeMace_Raising2,
Extra_Unarmed_Combat_Skill_Raising2,
CascadeSpear_Raising2,
CascadeStaff_Raising2,
Extra_Sword_Skill_Raising2,
Acid_Protection_Rare,
Acid_Resistance_Raising_Rare,
Alchemy_Raising_Rare,
Appraisal_Resistance_Lowering_Rare,
Appraise_Armor_Raising_Rare,
Appraise_Item_Raising_Rare,
Appraise_Magic_Item_Raising_Rare,
Appraise_Weapon_Raising_Rare,
Arcane_Lore_Raising_Rare,
Armor_Raising_Rare,
Armor_Value_Raising_Rare,
Assess_Monster_Raising_Rare,
Assess_Person_Raising_Rare,
Attack_Mod_Raising_Rare,
Axe_Raising_Rare,
Bludgeon_Protection_Rare,
Bludgeon_Resistance_Raising_Rare,
Bow_Raising_Rare,
Cold_Protection_Rare,
Cold_Resistance_Raising_Rare,
Cooking_Raising_Rare,
Coordination_Raising_Rare,
Creature_Enchantment_Raising_Rare,
Crossbow_Raising_Rare,
Dagger_Raising_Rare,
Damage_Raising_Rare,
Deception_Raising_Rare,
Defense_Mod_Raising_Rare,
Electric_Protection_Rare,
Electric_Resistance_Raising_Rare,
Endurance_Raising_Rare,
Fire_Protection_Rare,
Fire_Resistance_Raising_Rare,
Fletching_Raising_Rare,
Focus_Raising_Rare,
Healing_Raising_Rare,
Health_Accelerating_Rare,
Item_Enchantment_Raising_Rare,
Jump_Raising_Rare,
Leadership_Raising_Rare,
Life_Magic_Raising_Rare,
Lockpick_Raising_Rare,
Loyalty_Raising_Rare,
Mace_Raising_Rare,
Magic_Defense_Raising_Rare,
Mana_Accelerating_Rare,
Mana_Conversion_Raising_Rare,
Melee_Defense_Raising_Rare,
Missile_Defense_Raising_Rare,
Pierce_Protection_Rare,
Pierce_Resistance_Raising_Rare,
Quickness_Raising_Rare,
Run_Raising_Rare,
Self_Raising_Rare,
Slash_Protection_Rare,
Slash_Resistance_Raising_Rare,
Spear_Raising_Rare,
Staff_Raising_Rare,
Stamina_Accelerating_Rare,
Strength_Raising_Rare,
Sword_Raising_Rare,
Thrown_Weapons_Raising_Rare,
Unarmed_Combat_Raising_Rare,
War_Magic_Raising_Rare,
Weapon_Time_Raising_Rare,
Armor_Increase_Inky_Armor,
Magic_Defense_Shelter_Raising_Fiun,
Extra_Run_Skill_Raising_Fiun,
Extra_Mana_Conversion_Skill_Raising_Fiun,
Attributes_Increase_Cantrip1,
Extra_Melee_Defense_Skill_Raising2,
ACTDPurchaseRewardSpell,
ACTDPurchaseRewardSpellHealth,
SaltAsh_Attack_Mod_Raising,
Quickness_Increase2,
Extra_Alchemy_Skill_Raising2,
Extra_Cooking_Skill_Raising2,
Extra_Fletching_Skill_Raising2,
Extra_Lockpick_Skill_Raising2,
MucorManaWell,
Stamina_Restoring2,
Allegiance_Raising,
Health_DoT,
Health_DoT_Secondary,
Health_DoT_Tertiary,
Health_HoT,
Health_HoT_Secondary,
Health_HoT_Tertiary,
Health_Divide_Secondary,
Health_Divide_Tertiary,
SetSword_Raising,
SetAxe_Raising,
SetDagger_Raising,
SetMace_Raising,
SetSpear_Raising,
SetStaff_Raising,
SetUnarmed_Raising,
SetBow_Raising,
SetCrossbow_Raising,
SetThrown_Raising,
SetItemEnchantment_Raising,
SetCreatureEnchantment_Raising,
SetWarMagic_Raising,
SetLifeMagic_Raising,
SetMeleeDefense_Raising,
SetMissileDefense_Raising,
SetMagicDefense_Raising,
SetStamina_Accelerating,
SetCooking_Raising,
SetFletching_Raising,
SetLockpick_Raising,
SetAlchemy_Raising,
SetSalvaging_Raising,
SetArmorExpertise_Raising,
SetWeaponExpertise_Raising,
SetItemTinkering_Raising,
SetMagicItemExpertise_Raising,
SetLoyalty_Raising,
SetStrength_Raising,
SetEndurance_Raising,
SetCoordination_Raising,
SetQuickness_Raising,
SetFocus_Raising,
SetWillpower_Raising,
SetHealth_Raising,
SetStamina_Raising,
SetMana_Raising,
SetSprint_Raising,
SetJumping_Raising,
SetSlashResistance_Raising,
SetBludgeonResistance_Raising,
SetPierceResistance_Raising,
SetFlameResistance_Raising,
SetAcidResistance_Raising,
SetFrostResistance_Raising,
SetLightningResistance_Raising,
Crafting_LockPick_Raising,
Crafting_Fletching_Raising,
Crafting_Cooking_Raising,
Crafting_Alchemy_Raising,
Crafting_ArmorTinkering_Raising,
Crafting_WeaponTinkering_Raising,
Crafting_MagicTinkering_Raising,
Crafting_ItemTinkering_Raising,
SkillPercent_Alchemy_Raising,
TwoHanded_Raising,
TwoHanded_Lowering,
Extra_TwoHanded_Skill_Raising,
Extra_TwoHanded_Skill_Lowering,
Extra_TwoHanded_Skill_Raising2,
TwoHanded_Raising_Rare,
SetTwoHanded_Raising,
GearCraft_Raising,
GearCraft_Lowering,
Extra_GearCraft_Skill_Raising,
Extra_GearCraft_Skill_Lowering,
Extra_GearCraft_Skill_Raising2,
GearCraft_Raising_Rare,
SetGearCraft_Raising,
LoyaltyMana_Raising,
LoyaltyStamina_Raising,
LeadershipHealth_Raising,
TrinketDamage_Raising,
TrinketDamage_Lowering,
TrinketHealth_Raising,
TrinketStamina_Raising,
TrinketMana_Raising,
TrinketXP_Raising,
DeceptionArcaneLore_Raising,
HealOverTime_Raising,
DamageOverTime_Raising,
HealingResistRating_Raising,
AetheriaDamageRating_Raising,
AetheriaDamageReduction_Raising,
SKIPPED,
AetheriaHealth_Raising,
AetheriaStamina_Raising,
AetheriaMana_Raising,
AetheriaCriticalDamage_Raising,
AetheriaHealingAmplification_Raising,
AetheriaProcDamageRating_Raising,
AetheriaProcDamageReduction_Raising,
AetheriaProcHealthOverTime_Raising,
AetheriaProcDamageOverTime_Raising,
AetheriaProcHealingReduction_Raising,
RareDamageRating_Raising,
RareDamageReductionRating_Raising,
AetheriaEndurance_Raising,
NetherDamageOverTime_Raising,
NetherDamageOverTime_Raising2,
NetherDamageOverTime_Raising3,
Nether_Streak,
Nether_Missile,
Nether_Ring,
NetherDamageRating_Lowering,
NetherDamageHealingReduction_Raising,
Void_Magic_Lowering,
Void_Magic_Raising,
Void_Mystic_Raising,
SetVoidMagic_Raising,
Void_Magic_Raising_Rare,
Void_Mystic_Raising2,
LuminanceDamageRating_Raising,
LuminanceDamageReduction_Raising,
LuminanceHealth_Raising,
AetheriaCriticalReduction_Raising,
Extra_Missile_Defense_Skill_Raising,
Extra_Missile_Defense_Skill_Lowering,
Extra_Missile_Defense_Skill_Raising2,
AetheriaHealthResistance_Raising,
AetheriaDotResistance_Raising,
Cloak_Skill_Raising,
Cloak_All_Skill_Raising,
Cloak_Magic_Defense_Lowering,
Cloak_Melee_Defense_Lowering,
Cloak_Missile_Defense_Lowering,
DirtyFighting_Lowering,
DirtyFighting_Raising,
Extra_DirtyFighting_Raising,
DualWield_Lowering,
DualWield_Raising,
Extra_DualWield_Raising,
Recklessness_Lowering,
Recklessness_Raising,
Extra_Recklessness_Raising,
Shield_Lowering,
Shield_Raising,
Extra_Shield_Raising,
SneakAttack_Lowering,
SneakAttack_Raising,
Extra_SneakAttack_Raising,
Rare_DirtyFighting_Raising,
Rare_DualWield_Raising,
Rare_Recklessness_Raising,
Rare_Shield_Raising,
Rare_SneakAttack_Raising,
DF_Attack_Skill_Debuff,
DF_Bleed_Damage,
DF_Defense_Skill_Debuff,
DF_Healing_Debuff,
SetDirtyFighting_Raising,
SetDualWield_Raising,
SetRecklessness_Raising,
SetShield_Raising,
SetSneakAttack_Raising,
LifeGiver_Mhoire,
RareDamageRating_Raising2,
Spell_Damage_Raising,
Summoning_Raising,
Summoning_Lowering,
Extra_Summoning_Skill_Raising,
SetSummoning_Raising
}
}

View file

@ -0,0 +1,106 @@
using System.ComponentModel;
namespace Mag.Shared.Constants
{
// https://github.com/ACEmulator/ACE/blob/master/Source/ACE.Entity/Enum/Properties/PropertyString.cs
public enum StringValueKey
{
// properties marked as ServerOnly are properties we never saw in PCAPs, from here:
// http://ac.yotesfan.com/ace_object/not_used_enums.php
// source: @OptimShi
// description attributes are used by the weenie editor for a cleaner display name
Undef = 0,
[SendOnLogin]
Name = 1,
/// <summary>
/// default "Adventurer"
/// </summary>
Title = 2,
Sex = 3,
HeritageGroup = 4,
Template = 5,
AttackersName = 6,
Inscription = 7,
[Description("Scribe Name")]
ScribeName = 8,
VendorsName = 9,
Fellowship = 10,
MonarchsName = 11,
[ServerOnly]
LockCode = 12,
[ServerOnly]
KeyCode = 13,
Use = 14,
ShortDesc = 15,
LongDesc = 16,
ActivationTalk = 17,
[ServerOnly]
UseMessage = 18,
ItemHeritageGroupRestriction = 19,
PluralName = 20,
MonarchsTitle = 21,
ActivationFailure = 22,
ScribeAccount = 23,
TownName = 24,
CraftsmanName = 25,
UsePkServerError = 26,
ScoreCachedText = 27,
ScoreDefaultEntryFormat = 28,
ScoreFirstEntryFormat = 29,
ScoreLastEntryFormat = 30,
ScoreOnlyEntryFormat = 31,
ScoreNoEntry = 32,
[ServerOnly]
Quest = 33,
GeneratorEvent = 34,
PatronsTitle = 35,
HouseOwnerName = 36,
QuestRestriction = 37,
AppraisalPortalDestination = 38,
TinkerName = 39,
ImbuerName = 40,
HouseOwnerAccount = 41,
DisplayName = 42,
DateOfBirth = 43,
ThirdPartyApi = 44,
KillQuest = 45,
Afk = 46,
AllegianceName = 47,
AugmentationAddQuest = 48,
KillQuest2 = 49,
KillQuest3 = 50,
UseSendsSignal = 51,
[Description("Gear Plating Name")]
GearPlatingName = 52,
// ACE Specific
[ServerOnly]
PCAPRecordedCurrentMotionState = 8006,
[ServerOnly]
PCAPRecordedServerName = 8031,
[ServerOnly]
PCAPRecordedCharacterName = 8032,
/* custom */
[ServerOnly]
AllegianceMotd = 9001,
[ServerOnly]
AllegianceMotdSetBy = 9002,
[ServerOnly]
AllegianceSpeakerTitle = 9003,
[ServerOnly]
AllegianceSeneschalTitle = 9004,
[ServerOnly]
AllegianceCastellanTitle = 9005,
[ServerOnly]
GodState = 9006,
[ServerOnly]
TinkerLog = 9007,
// Decal Specific
SecondaryName_Decal = 184549376,
}
}

View file

@ -0,0 +1,79 @@
namespace Mag.Shared.Constants
{
public enum WeenieType : uint
{
Undef,
Generic,
Clothing,
MissileLauncher,
Missile,
Ammunition,
MeleeWeapon,
Portal,
Book,
Coin,
Creature,
Admin,
Vendor,
HotSpot,
Corpse,
Cow,
AI,
Machine,
Food,
Door,
Chest,
Container,
Key,
Lockpick,
PressurePlate,
LifeStone,
Switch,
PKModifier,
Healer,
LightSource,
Allegiance,
UNKNOWN__GUESSEDNAME32, // NOTE: Missing 1
SpellComponent,
ProjectileSpell,
Scroll,
Caster,
Channel,
ManaStone,
Gem,
AdvocateFane,
AdvocateItem,
Sentinel,
GSpellEconomy,
LSpellEconomy,
CraftTool,
LScoreKeeper,
GScoreKeeper,
GScoreGatherer,
ScoreBook,
EventCoordinator,
Entity,
Stackable,
HUD,
House,
Deed,
SlumLord,
Hook,
Storage,
BootSpot,
HousePortal,
Game,
GamePiece,
SkillAlterationDevice,
AttributeTransferDevice,
Hooker,
AllegianceBindstone,
InGameStatKeeper,
AugmentationDevice,
SocialManager,
Pet,
PetDevice,
CombatPet
}
}

View file

@ -0,0 +1,20 @@
namespace Mag.Shared.Constants
{
public enum WieldRequirement
{
Invalid,
Skill,
RawSkill,
Attrib,
RawAttrib,
SecondaryAttrib,
RawSecondaryAttrib,
Level,
Training,
IntStat,
BoolStat,
CreatureType,
HeritageType
}
}

139
Shared/Debug.cs Normal file
View file

@ -0,0 +1,139 @@
using System;
using System.Globalization;
using System.IO;
using Decal.Adapter;
namespace Mag.Shared
{
static class Debug
{
static string _debugLogPath;
static string _errorLogPath;
static string _pluginName;
public static void Init(string debugLogPath, string errorLogPath, string pluginName)
{
_debugLogPath = debugLogPath;
_errorLogPath = errorLogPath;
_pluginName = pluginName;
}
/// <summary>
/// This will write to the debug.txt
/// </summary>
public static void LogDebug(string message)
{
try
{
if (String.IsNullOrEmpty(_debugLogPath))
return;
FileInfo fileInfo = new FileInfo(_debugLogPath);
// Limit the file to 1MB
bool append = !(fileInfo.Exists && fileInfo.Length > 1048576);
using (StreamWriter writer = new StreamWriter(fileInfo.FullName, append))
{
writer.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + "," + message);
writer.Close();
}
}
catch
{
// Eat the exception, yumm.
}
}
/// <summary>
/// This will only write the exception to the errors.txt file if DebugEnabled is true.
/// </summary>
public static void LogException(Exception ex, string note = null)
{
try
{
//if (!Settings.SettingsManager.Misc.DebuggingEnabled.Value)
// return;
if (note != null)
MyClasses.VCS_Connector.SendChatTextCategorized("Errors", "<{" + _pluginName + "}>: " + "Exception caught: " + ex.Message + Environment.NewLine + ex.Source + Environment.NewLine + ex.StackTrace + Environment.NewLine + "Note: " + note, 5);
else
MyClasses.VCS_Connector.SendChatTextCategorized("Errors", "<{" + _pluginName + "}>: " + "Exception caught: " + ex.Message + Environment.NewLine + ex.Source + Environment.NewLine + ex.StackTrace, 5);
if (String.IsNullOrEmpty(_errorLogPath))
return;
FileInfo fileInfo = new FileInfo(_errorLogPath);
// Limit the file to 1MB
bool append = !(fileInfo.Exists && fileInfo.Length > 1048576);
using (StreamWriter writer = new StreamWriter(fileInfo.FullName, append))
{
writer.WriteLine("============================================================================");
writer.WriteLine(DateTime.Now.ToString(CultureInfo.InvariantCulture));
writer.WriteLine(ex);
if (note != null)
writer.WriteLine("Note: " + note);
writer.WriteLine("============================================================================");
writer.WriteLine("");
writer.Close();
}
}
catch
{
// Eat the exception, yumm.
}
}
public static void LogText(string text)
{
try
{
//if (!Settings.SettingsManager.Misc.DebuggingEnabled.Value)
// return;
MyClasses.VCS_Connector.SendChatTextCategorized("CommandLine", "<{" + _pluginName + "}>: " + "Log Text: " + text, 5);
if (String.IsNullOrEmpty(_errorLogPath))
return;
FileInfo fileInfo = new FileInfo(_errorLogPath);
// Limit the file to 1MB
bool append = !(fileInfo.Exists && fileInfo.Length > 1048576);
using (StreamWriter writer = new StreamWriter(fileInfo.FullName, append))
{
writer.WriteLine(DateTime.Now + ": " + text);
writer.Close();
}
}
catch (Exception ex) { LogException(ex); }
}
/// <summary>
/// This will only write the message to the chat if DebugEnabled is true.
/// </summary>
/// <param name="message"></param>
/// <param name="color"></param>
/// <param name="target"></param>
public static void WriteToChat(string message, int color = 5, int target = 1)
{
try
{
//if (!Settings.SettingsManager.Misc.DebuggingEnabled.Value)
// return;
MyClasses.VCS_Connector.SendChatTextCategorized("CommandLine", "<{" + _pluginName + "}>: " + message, color, target);
}
catch (Exception ex) { LogException(ex); }
}
}
}

178
Shared/DecalProxy.cs Normal file
View file

@ -0,0 +1,178 @@
///////////////////////////////////////////////////////////////////////////////
//File: DecalProxy.cs
//
//Description: Contains reflection-based calls of newer Decal methods, so that
// they can be easily invoked only if the currently running version contains
// the desired feature.
//
//
//This file is Copyright (c) 2013 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.
///////////////////////////////////////////////////////////////////////////////
//Virindi: yes, or you could just directly resize the window
//Virindi: note...
//Virindi: right now you have to call move after you call resize
//Virindi: so right now what you have to do if you use that resize is grab the location, resize, then move back to the original location
using System;
using System.Runtime.InteropServices;
using System.Reflection;
using System.Drawing;
using Decal.Adapter;
namespace Mag.Shared
{
internal static class DecalProxy
{
public enum UIElementType : int
{
Smartbox = 0x1000049A,
Chat = 0x10000601,
FloatChat1 = 0x10000505,
FloatChat2 = 0x1000050E,
FloatChat3 = 0x1000050F,
FloatChat4 = 0x10000510,
Examination = 0x100005F7,
Vitals = 0x100005FA,
EnvPack = 0x100005FD,
Panels = 0x100005FF,
TBar = 0x10000603,
Indicators = 0x10000611,
ProgressBar = 0x10000613,
Combat = 0x100006B5,
Radar = 0x100006D2,
}
static Version iCachedDecalVersion = null;
static Assembly iCachedDecalAssembly = null;
public static bool TestDecalVersion(Version v)
{
if (iCachedDecalVersion != null) return iCachedDecalVersion >= v;
System.Reflection.Assembly[] asms = AppDomain.CurrentDomain.GetAssemblies();
foreach (System.Reflection.Assembly a in asms)
{
AssemblyName nmm = a.GetName();
if (nmm.Name == "Decal.Adapter")
{
iCachedDecalVersion = nmm.Version;
iCachedDecalAssembly = a;
return (nmm.Version >= v);
}
}
return false;
}
public static void Hooks_UIElementMove(Decal.Adapter.Wrappers.HooksWrapper hooks, UIElementType e, int x, int y)
{
//This will load the cached assembly. We don't actually care what the version is
//since we just check below if the required members are present.
TestDecalVersion(new Version());
int ee = (int)e;
Type DecalType_UIElementType = iCachedDecalAssembly.GetType("Decal.Adapter.Wrappers.UIElementType");
if (DecalType_UIElementType == null) return;
object e_decal = Enum.ToObject(DecalType_UIElementType, ee);
Type DecalType_HooksWrapper = iCachedDecalAssembly.GetType("Decal.Adapter.Wrappers.HooksWrapper");
if (DecalType_HooksWrapper == null) return;
MethodInfo call = DecalType_HooksWrapper.GetMethod("UIElementMove", new Type[] { DecalType_UIElementType, typeof(int), typeof(int) });
if (call == null) return;
call.Invoke(hooks, new object[] { e_decal, x, y });
}
public static void Hooks_UIElementResize(Decal.Adapter.Wrappers.HooksWrapper hooks, UIElementType e, int width, int height)
{
//This will load the cached assembly. We don't actually care what the version is
//since we just check below if the required members are present.
TestDecalVersion(new Version());
int ee = (int)e;
Type DecalType_UIElementType = iCachedDecalAssembly.GetType("Decal.Adapter.Wrappers.UIElementType");
if (DecalType_UIElementType == null) return;
object e_decal = Enum.ToObject(DecalType_UIElementType, ee);
Type DecalType_HooksWrapper = iCachedDecalAssembly.GetType("Decal.Adapter.Wrappers.HooksWrapper");
if (DecalType_HooksWrapper == null) return;
MethodInfo call = DecalType_HooksWrapper.GetMethod("UIElementResize", new Type[] { DecalType_UIElementType, typeof(int), typeof(int) });
if (call == null) return;
call.Invoke(hooks, new object[] { e_decal, width, height });
}
public static Rectangle Hooks_UIElementRegion(Decal.Adapter.Wrappers.HooksWrapper hooks, UIElementType e)
{
//This will load the cached assembly. We don't actually care what the version is
//since we just check below if the required members are present.
TestDecalVersion(new Version());
int ee = (int)e;
Type DecalType_UIElementType = iCachedDecalAssembly.GetType("Decal.Adapter.Wrappers.UIElementType");
if (DecalType_UIElementType == null) return Rectangle.Empty;
object e_decal = Enum.ToObject(DecalType_UIElementType, ee);
Type DecalType_HooksWrapper = iCachedDecalAssembly.GetType("Decal.Adapter.Wrappers.HooksWrapper");
if (DecalType_HooksWrapper == null) return Rectangle.Empty;
MethodInfo call = DecalType_HooksWrapper.GetMethod("UIElementRegion", new Type[] { DecalType_UIElementType });
if (call == null) return Rectangle.Empty;
return (Rectangle)call.Invoke(hooks, new object[] { e_decal });
}
[DllImport("Decal.dll")]
static extern int DispatchOnChatCommand(ref IntPtr str, [MarshalAs(UnmanagedType.U4)] int target);
static bool Decal_DispatchOnChatCommand(string cmd)
{
IntPtr bstr = Marshal.StringToBSTR(cmd);
try
{
bool eaten = (DispatchOnChatCommand(ref bstr, 1) & 0x1) > 0;
return eaten;
}
finally
{
Marshal.FreeBSTR(bstr);
}
}
/// <summary>
/// This will first attempt to send the messages to all plugins. If no plugins set e.Eat to true on the message, it will then simply call InvokeChatParser.
/// </summary>
/// <param name="cmd"></param>
public static void DispatchChatToBoxWithPluginIntercept(string cmd)
{
if (!Decal_DispatchOnChatCommand(cmd))
CoreManager.Current.Actions.InvokeChatParser(cmd);
}
}
}

355
Shared/ItemInfo.cs Normal file
View file

@ -0,0 +1,355 @@
using System;
using System.Collections.Generic;
using Mag.Shared.Constants;
using Mag.Shared.Spells;
namespace Mag.Shared
{
/// <summary>
/// Instantiate this object with the item you want info for.
/// ToString() this object for the info.
/// </summary>
public class ItemInfo
{
private readonly MyWorldObject mwo;
public ItemInfo(MyWorldObject myWorldObject)
{
mwo = myWorldObject;
}
public override string ToString()
{
return ToString(true, true);
}
public string ToString(bool showBuffedValues, bool showValueAndBurden)
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
if (mwo.Values(IntValueKey.MaterialType) > 0)
{
if (Dictionaries.MaterialInfo.ContainsKey(mwo.Values(IntValueKey.MaterialType)))
sb.Append(Dictionaries.MaterialInfo[mwo.Values(IntValueKey.MaterialType)] + " ");
else
sb.Append("unknown material " + mwo.Values(IntValueKey.MaterialType) + " ");
}
sb.Append(mwo.Name);
if (mwo.Values((IntValueKey)353) > 0)
{
if (Dictionaries.MasteryInfo.ContainsKey(mwo.Values((IntValueKey)353)))
sb.Append(" (" + Dictionaries.MasteryInfo[mwo.Values((IntValueKey)353)] + ")");
else
sb.Append(" (Unknown mastery " + mwo.Values((IntValueKey)353) + ")");
}
int set = mwo.Values((IntValueKey)265, 0);
if (set != 0)
{
sb.Append(", ");
if (Dictionaries.AttributeSetInfo.ContainsKey(set))
sb.Append(Dictionaries.AttributeSetInfo[set]);
else
sb.Append("Unknown set " + set);
}
if (mwo.Values(IntValueKey.ArmorLevel) > 0)
sb.Append(", AL " + mwo.Values(IntValueKey.ArmorLevel));
if (mwo.Values(IntValueKey.ImbuedEffect) > 0)
{
sb.Append(",");
if ((mwo.Values(IntValueKey.ImbuedEffect) & 1) == 1) sb.Append(" CS");
if ((mwo.Values(IntValueKey.ImbuedEffect) & 2) == 2) sb.Append(" CB");
if ((mwo.Values(IntValueKey.ImbuedEffect) & 4) == 4) sb.Append(" AR");
if ((mwo.Values(IntValueKey.ImbuedEffect) & 8) == 8) sb.Append(" SlashRend");
if ((mwo.Values(IntValueKey.ImbuedEffect) & 16) == 16) sb.Append(" PierceRend");
if ((mwo.Values(IntValueKey.ImbuedEffect) & 32) == 32) sb.Append(" BludgeRend");
if ((mwo.Values(IntValueKey.ImbuedEffect) & 64) == 64) sb.Append(" AcidRend");
if ((mwo.Values(IntValueKey.ImbuedEffect) & 128) == 128) sb.Append(" FrostRend");
if ((mwo.Values(IntValueKey.ImbuedEffect) & 256) == 256) sb.Append(" LightRend");
if ((mwo.Values(IntValueKey.ImbuedEffect) & 512) == 512) sb.Append(" FireRend");
if ((mwo.Values(IntValueKey.ImbuedEffect) & 1024) == 1024) sb.Append(" MeleeImbue");
if ((mwo.Values(IntValueKey.ImbuedEffect) & 4096) == 4096) sb.Append(" MagicImbue");
if ((mwo.Values(IntValueKey.ImbuedEffect) & 8192) == 8192) sb.Append(" Hematited");
if ((mwo.Values(IntValueKey.ImbuedEffect) & 536870912) == 536870912) sb.Append(" MagicAbsorb");
}
if (mwo.Values(IntValueKey.NumTimesTinkered) > 0)
sb.Append(", Tinks " + mwo.Values(IntValueKey.NumTimesTinkered));
if (mwo.Values(IntValueKey.MaxDamage_Decal) != 0 && mwo.Values(DoubleValueKey.Variance_Decal) != 0)
sb.Append(", " + (mwo.Values(IntValueKey.MaxDamage_Decal) - (mwo.Values(IntValueKey.MaxDamage_Decal) * mwo.Values(DoubleValueKey.Variance_Decal))).ToString("N2") + "-" + mwo.Values(IntValueKey.MaxDamage_Decal));
else if (mwo.Values(IntValueKey.MaxDamage_Decal) != 0 && mwo.Values(DoubleValueKey.Variance_Decal) == 0)
sb.Append(", " + mwo.Values(IntValueKey.MaxDamage_Decal));
if (mwo.Values(IntValueKey.ElementalDamageBonus, 0) != 0)
sb.Append(", +" + mwo.Values(IntValueKey.ElementalDamageBonus));
if (mwo.Values(DoubleValueKey.DamageBonus_Decal, 1) != 1)
sb.Append(", +" + Math.Round(((mwo.Values(DoubleValueKey.DamageBonus_Decal) - 1) * 100)) + "%");
if (mwo.Values(DoubleValueKey.ElementalDamageMod, 1) != 1)
sb.Append(", +" + Math.Round(((mwo.Values(DoubleValueKey.ElementalDamageMod) - 1) * 100)) + "%vs. Monsters");
if (mwo.Values(DoubleValueKey.AttackBonus_Decal, 1) != 1)
sb.Append(", +" + Math.Round(((mwo.Values(DoubleValueKey.AttackBonus_Decal) - 1) * 100)) + "%a");
if (mwo.Values(DoubleValueKey.WeaponDefense, 1) != 1)
sb.Append(", " + Math.Round(((mwo.Values(DoubleValueKey.WeaponDefense) - 1) * 100)) + "%md");
if (mwo.Values(DoubleValueKey.WeaponMagicDefense, 1) != 1)
sb.Append(", " + Math.Round(((mwo.Values(DoubleValueKey.WeaponMagicDefense) - 1) * 100), 1) + "%mgc.d");
if (mwo.Values(DoubleValueKey.WeaponMissileDefense, 1) != 1)
sb.Append(", " + Math.Round(((mwo.Values(DoubleValueKey.WeaponMissileDefense) - 1) * 100), 1) + "%msl.d");
if (mwo.Values(DoubleValueKey.ManaConversionMod) != 0)
sb.Append(", " + Math.Round((mwo.Values(DoubleValueKey.ManaConversionMod) * 100)) + "%mc");
if (showBuffedValues && (mwo.ObjectClass == (int)ObjectClass.MeleeWeapon || mwo.ObjectClass == (int)ObjectClass.MissileWeapon || mwo.ObjectClass == (int)ObjectClass.WandStaffOrb))
{
sb.Append(", (");
// (Damage)
if (mwo.ObjectClass == (int)ObjectClass.MeleeWeapon)
sb.Append(mwo.CalcedBuffedTinkedDoT.ToString("N1") + "/" + mwo.GetBuffedIntValueKey((int)IntValueKey.MaxDamage_Decal));
if (mwo.ObjectClass == (int)ObjectClass.MissileWeapon)
sb.Append(mwo.CalcedBuffedMissileDamage.ToString("N1"));
if (mwo.ObjectClass == (int)ObjectClass.WandStaffOrb)
sb.Append(((mwo.GetBuffedDoubleValueKey((int)DoubleValueKey.ElementalDamageMod) - 1) * 100).ToString("N1"));
// (AttackBonus/MeleeDefenseBonus/ManaCBonus)
sb.Append(" ");
if (mwo.Values(DoubleValueKey.AttackBonus_Decal, 1) != 1)
sb.Append(Math.Round(((mwo.GetBuffedDoubleValueKey((int)DoubleValueKey.AttackBonus_Decal) - 1) * 100)).ToString("N1") + "/");
if (mwo.Values(DoubleValueKey.WeaponDefense, 1) != 1)
sb.Append(Math.Round(((mwo.GetBuffedDoubleValueKey((int)DoubleValueKey.WeaponDefense) - 1) * 100)).ToString("N1"));
if (mwo.Values(DoubleValueKey.ManaConversionMod) != 0)
sb.Append("/" + Math.Round(mwo.GetBuffedDoubleValueKey((int)DoubleValueKey.ManaConversionMod) * 100));
sb.Append(")");
}
if (mwo.Spells.Count > 0)
{
List<int> sortedSpellIds = new List<int>();
foreach (var spell in mwo.Spells)
sortedSpellIds.Add(spell);
sortedSpellIds.Sort();
sortedSpellIds.Reverse();
foreach (int spellId in sortedSpellIds)
{
Spell spell = SpellTools.GetSpell(spellId);
if (spell == null)
continue;
// If the item is not loot generated, show all spells
if (!mwo.IntValues.ContainsKey((int)IntValueKey.MaterialType))
goto ShowSpell;
// Always show Minor/Major/Epic Impen
if (spell.Name.Contains("Minor Impenetrability") || spell.Name.Contains("Major Impenetrability") || spell.Name.Contains("Epic Impenetrability") || spell.Name.Contains("Legendary Impenetrability"))
goto ShowSpell;
// Always show trinket spells
if (spell.Name.Contains("Augmented"))
goto ShowSpell;
if (mwo.Values(IntValueKey.ResistMagic, 0) != 0)
{
// Show banes and impen on unenchantable equipment
if (spell.Name.Contains(" Bane") || spell.Name.Contains("Impen") || spell.Name.StartsWith("Brogard"))
goto ShowSpell;
}
else
{
// Hide banes and impen on enchantable equipment
if (spell.Name.Contains(" Bane") || spell.Name.Contains("Impen") || spell.Name.StartsWith("Brogard"))
continue;
}
//Debug.WriteToChat(spellById.Name + ", Difficulty: " + spellById.Difficulty + ", Family: " + spellById.Family + ", Generation: " + spellById.Generation + ", Type: " + spellById.Type + ", " + spellById.Unknown1 + " " + spellById.Unknown2 + " " + spellById.Unknown3 + " " + spellById.Unknown4 + " " + spellById.Unknown5 + " " + spellById.Unknown6 + " " + spellById.Unknown7 + " " + spellById.Unknown8 + " " + spellById.Unknown9 + " " + spellById.Unknown10);
// <{Mag-Tools}>: Major Coordination, Difficulty: 15, Family: 267, Generation: 1, Type: 1, 0 1 1 2572 -2.07525870829232E+20 0 0 0 0 0
// <{Mag-Tools}>: Epic Magic Resistance, Difficulty: 20, Family: 299, Generation: 1, Type: 1, 0 1 1 4704 -2.07525870829232E+20 0 0 0 0 0
// <{Mag-Tools}>: Epic Life Magic Aptitude, Difficulty: 20, Family: 357, Generation: 1, Type: 1, 0 1 1 4700 -2.07525870829232E+20 0 0 0 0 0
// <{Mag-Tools}>: Epic Endurance, Difficulty: 20, Family: 263, Generation: 1, Type: 1, 0 1 1 4226 -2.07525870829232E+20 0 0 0 0 0
// <{Mag-Tools}>: Essence Glutton, Difficulty: 30, Family: 279, Generation: 1, Type: 1, 0 0 1 2666 -2.07525870829232E+20 0 0 0 0 0
// <{Mag-Tools}>: Might of the Lugians, Difficulty: 300, Family: 1, Generation: 1, Type: 1, 0 0 1 2087 -2.07525870829232E+20 0 0 0 0 0
// <{Mag-Tools}>: Executor's Blessing, Difficulty: 300, Family: 115, Generation: 1, Type: 1, 0 0 1 2053 -2.07525870829232E+20 0 0 0 0 0
// <{Mag-Tools}>: Regeneration Other Incantation, Difficulty: 400, Family: 93, Generation: 1, Type: 1, 5 0.25 1 3982 -2.07525870829232E+20 0 0 0 0 0
// Focusing stone
// <{Mag-Tools}>: Brilliance, Difficulty: 250, Family: 15, Generation: 1, Type: 1, 5 0.25 1 2348 -2.07525870829232E+20 0 0 0 0 0
// <{Mag-Tools}>: Concentration, Difficulty: 100, Family: 13, Generation: 1, Type: 1, 0 0 1 2347 -2.07525870829232E+20 0 0 0 0 0
// <{Mag-Tools}>: Malediction, Difficulty: 50, Family: 284, Generation: 1, Type: 1, 0 0 1 2346 -2.07525870829232E+20 0 0 0 0 0
// Weapon buffs
// <{Mag-Tools}>: Elysa's Sight, Difficulty: 300, Family: 152, Generation: 1, Type: 1, 25 0 1 2106 -2.07525870829232E+20 0 0 0 0 0 (Attack Skill)
// <{Mag-Tools}>: Infected Caress, Difficulty: 300, Family: 154, Generation: 1, Type: 1, 25 0 1 2096 -2.07525870829232E+20 0 0 0 0 0 (Damage)
// <{Mag-Tools}>: Infected Spirit Caress, Difficulty: 300, Family: 154, Generation: 1, Type: 1, 25 0 1 3259 -2.07525870829232E+20 0 0 0 0 0 (Damage)
// <{Mag-Tools}>: Cragstone's Will, Difficulty: 300, Family: 156, Generation: 1, Type: 1, 25 0 1 2101 -2.07525870829232E+20 0 0 0 0 0 (Defense)
// <{Mag-Tools}>: Atlan's Alacrity, Difficulty: 300, Family: 158, Generation: 1, Type: 1, 25 0 1 2116 -2.07525870829232E+20 0 0 0 0 0 (Speed)
// <{Mag-Tools}>: Mystic's Blessing, Difficulty: 300, Family: 195, Generation: 1, Type: 1, 25 0 1 2117 -2.07525870829232E+20 0 0 0 0 0 (Mana C)
// <{Mag-Tools}>: Vision of the Hunter, Difficulty: 500, Family: 325, Generation: 1, Type: 1, 25 0 1 2968 -2.07525870829232E+20 0 0 0 0 0 (Damage Mod)
if ((spell.Family >= 152 && spell.Family <= 158) || spell.Family == 195 || spell.Family == 325)
{
// This is a weapon buff
// Lvl 6
if (spell.Difficulty == 250)
continue;
// Lvl 7
if (spell.Difficulty == 300)
goto ShowSpell;
// Lvl 8+
if (spell.Difficulty >= 400)
goto ShowSpell;
continue;
}
// This is not a weapon buff.
// Filter all 1-5 spells
if (spell.Name.EndsWith(" I") || spell.Name.EndsWith(" II") || spell.Name.EndsWith(" III") || spell.Name.EndsWith(" IV") || spell.Name.EndsWith(" V"))
continue;
// Filter 6's
if (spell.Name.EndsWith(" VI"))
continue;
// Filter 7's
if (spell.Difficulty == 300)
continue;
// Filter 8's
if (spell.Name.Contains("Incantation"))
continue;
ShowSpell:
sb.Append(", " + spell.Name);
}
}
// Wield Lvl 180
if (mwo.Values(IntValueKey.WieldDifficulty) > 0)
{
// I don't quite understand this.
if (mwo.Values(IntValueKey.WieldRequirements) == 7 && mwo.Values(IntValueKey.WieldSkillType) == 1)
sb.Append(", Wield Lvl " + mwo.Values(IntValueKey.WieldDifficulty));
else
{
if (Dictionaries.SkillInfo.ContainsKey(mwo.Values(IntValueKey.WieldSkillType)))
sb.Append(", " + Dictionaries.SkillInfo[mwo.Values(IntValueKey.WieldSkillType)] + " " + mwo.Values(IntValueKey.WieldDifficulty));
else
sb.Append(", Unknown skill: " + mwo.Values(IntValueKey.WieldSkillType) + " " + mwo.Values(IntValueKey.WieldDifficulty));
}
}
// Summoning Gem
if (mwo.Values((IntValueKey)369) > 0)
sb.Append(", Lvl " + mwo.Values((IntValueKey)369));
// Melee Defense 300 to Activate
// If the activation is lower than the wield requirement, don't show it.
if (mwo.Values(IntValueKey.ItemSkillLevelLimit) > 0 && (mwo.Values(IntValueKey.WieldSkillType) != mwo.Values(IntValueKey.AppraisalItemSkill) || mwo.Values(IntValueKey.WieldDifficulty) < mwo.Values(IntValueKey.ItemSkillLevelLimit)))
{
if (Dictionaries.SkillInfo.ContainsKey(mwo.Values(IntValueKey.AppraisalItemSkill)))
sb.Append(", " + Dictionaries.SkillInfo[mwo.Values(IntValueKey.AppraisalItemSkill)] + " " + mwo.Values(IntValueKey.ItemSkillLevelLimit) + " to Activate");
else
sb.Append(", Unknown skill: " + mwo.Values(IntValueKey.AppraisalItemSkill) + " " + mwo.Values(IntValueKey.ItemSkillLevelLimit) + " to Activate");
}
// Summoning Gem
if (mwo.Values((IntValueKey)366) > 0 && mwo.Values((IntValueKey)367) > 0)
{
if (Dictionaries.SkillInfo.ContainsKey(mwo.Values((IntValueKey)366)))
sb.Append(", " + Dictionaries.SkillInfo[mwo.Values((IntValueKey)366)] + " " + mwo.Values((IntValueKey)367));
else
sb.Append(", Unknown skill: " + mwo.Values((IntValueKey)366) + " " + mwo.Values((IntValueKey)367));
}
// Summoning Gem
if (mwo.Values((IntValueKey)368) > 0 && mwo.Values((IntValueKey)367) > 0)
{
if (Dictionaries.SkillInfo.ContainsKey(mwo.Values((IntValueKey)368)))
sb.Append(", Spec " + Dictionaries.SkillInfo[mwo.Values((IntValueKey)368)] + " " + mwo.Values((IntValueKey)367));
else
sb.Append(", Unknown skill spec: " + mwo.Values((IntValueKey)368) + " " + mwo.Values((IntValueKey)367));
}
if (mwo.Values(IntValueKey.ItemDifficulty) > 0)
sb.Append(", Diff " + mwo.Values(IntValueKey.ItemDifficulty));
if (mwo.ObjectClass == (int)ObjectClass.Salvage)
{
if (mwo.Values(DoubleValueKey.SalvageWorkmanship_Decal) > 0)
sb.Append(", Work " + mwo.Values(DoubleValueKey.SalvageWorkmanship_Decal).ToString("N2"));
}
else
{
if (mwo.Values(IntValueKey.ItemWorkmanship) > 0 && mwo.Values(IntValueKey.NumTimesTinkered) != 10) // Don't show the work if its already 10 tinked.
sb.Append(", Craft " + mwo.Values(IntValueKey.ItemWorkmanship));
}
if (mwo.ObjectClass == (int)ObjectClass.Armor && mwo.Values(IntValueKey.ResistMagic, 0) != 0)
{
sb.Append(", [" +
mwo.Values(DoubleValueKey.SlashProt_Decal).ToString("N1") + "/" +
mwo.Values(DoubleValueKey.PierceProt_Decal).ToString("N1") + "/" +
mwo.Values(DoubleValueKey.BludgeonProt_Decal).ToString("N1") + "/" +
mwo.Values(DoubleValueKey.ColdProt_Decal).ToString("N1") + "/" +
mwo.Values(DoubleValueKey.FireProt_Decal).ToString("N1") + "/" +
mwo.Values(DoubleValueKey.AcidProt_Decal).ToString("N1") + "/" +
mwo.Values(DoubleValueKey.LightningProt_Decal).ToString("N1") + "]");
}
if (showValueAndBurden)
{
if (mwo.Values(IntValueKey.Value) > 0)
sb.Append(", Value " + String.Format("{0:n0}", mwo.Values(IntValueKey.Value)));
if (mwo.Values(IntValueKey.EncumbranceVal) > 0)
sb.Append(", BU " + mwo.Values(IntValueKey.EncumbranceVal));
}
if (mwo.TotalRating > 0)
{
sb.Append(", [");
bool first = true;
if (mwo.DamRating > 0) { sb.Append("D " + mwo.DamRating); first = false; }
if (mwo.DamResistRating > 0) { if (!first) sb.Append(", "); sb.Append("DR " + mwo.DamResistRating); first = false; }
if (mwo.CritRating > 0) { if (!first) sb.Append(", "); sb.Append("C " + mwo.CritRating); first = false; }
if (mwo.CritDamRating > 0) { if (!first) sb.Append(", "); sb.Append("CD " + mwo.CritDamRating); first = false; }
if (mwo.CritResistRating > 0) { if (!first) sb.Append(", "); sb.Append("CR " + mwo.CritResistRating); first = false; }
if (mwo.CritDamResistRating > 0) { if (!first) sb.Append(", "); sb.Append("CDR " + mwo.CritDamResistRating); first = false; }
if (mwo.HealBoostRating > 0) { if (!first) sb.Append(", "); sb.Append("HB " + mwo.HealBoostRating); first = false; }
if (mwo.VitalityRating > 0) { if (!first) sb.Append(", "); sb.Append("V " + mwo.VitalityRating); first = false; }
sb.Append("]");
}
if (mwo.ObjectClass == (int)ObjectClass.Misc && mwo.Name.Contains("Keyring"))
sb.Append(", Keys: " + mwo.Values(IntValueKey.NumKeys) + ", Uses: " + mwo.Values(IntValueKey.Structure));
return sb.ToString();
}
}
}

388
Shared/MyWorldObject.cs Normal file
View file

@ -0,0 +1,388 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Mag.Shared.Constants;
namespace Mag.Shared
{
/// <summary>
/// This is a user defiend object to represent a Decal.Adapter.WorldObject.
/// All properties will return -1 if the property isn't supported.
/// </summary>
[Serializable]
public class MyWorldObject
{
public bool HasIdData;
public int Id;
public int LastIdTime;
public int ObjectClass;
public int Icon;
public SerializableDictionary<int, bool> BoolValues = new SerializableDictionary<int, bool>();
public SerializableDictionary<int, double> DoubleValues = new SerializableDictionary<int, double>();
public SerializableDictionary<int, int> IntValues = new SerializableDictionary<int, int>();
public SerializableDictionary<int, string> StringValues = new SerializableDictionary<int, string>();
public List<int> ActiveSpells = new List<int>();
public List<int> Spells = new List<int>();
public void Init(bool hasIdData, int id, int lastIdTime, int objectClass, int icon, IDictionary<int, bool> boolValues, IDictionary<int, double> doubleValues, IDictionary<int, int> intValues, IDictionary<int, string> stringValues, IList<int> activeSpells, IList<int> spells)
{
HasIdData = hasIdData;
Id = id;
LastIdTime = lastIdTime;
ObjectClass = objectClass;
Icon = icon;
AddTo(boolValues, doubleValues, intValues, stringValues);
ActiveSpells.Clear();
foreach (var i in activeSpells)
ActiveSpells.Add(i);
Spells.Clear();
foreach (var i in spells)
Spells.Add(i);
}
public void AddTo(IDictionary<int, bool> boolValues, IDictionary<int, double> doubleValues, IDictionary<int, int> intValues, IDictionary<int, string> stringValues)
{
foreach (var kvp in boolValues)
{
if (boolValues.ContainsKey(kvp.Key))
BoolValues[kvp.Key] = kvp.Value;
else
BoolValues.Add(kvp.Key, kvp.Value);
}
foreach (var kvp in doubleValues)
{
if (doubleValues.ContainsKey(kvp.Key))
DoubleValues[kvp.Key] = kvp.Value;
else
DoubleValues.Add(kvp.Key, kvp.Value);
}
foreach (var kvp in intValues)
{
if (intValues.ContainsKey(kvp.Key))
IntValues[kvp.Key] = kvp.Value;
else
IntValues.Add(kvp.Key, kvp.Value);
}
foreach (var kvp in stringValues)
{
if (stringValues.ContainsKey(kvp.Key))
StringValues[kvp.Key] = kvp.Value;
else
StringValues.Add(kvp.Key, kvp.Value);
}
}
public bool Values(BoolValueKey key, bool defaultValue = false)
{
if (BoolValues.ContainsKey((int)key))
return BoolValues[(int)key];
return defaultValue;
}
public double Values(DoubleValueKey key, double defaultValue = 0)
{
if (DoubleValues.ContainsKey((int)key))
return DoubleValues[(int)key];
return defaultValue;
}
public int Values(IntValueKey key, int defaultValue = 0)
{
if (IntValues.ContainsKey((int)key))
return IntValues[(int)key];
return defaultValue;
}
public string Values(StringValueKey key, string defaultValue = null)
{
if (StringValues.ContainsKey((int)key))
return StringValues[(int)key];
return defaultValue;
}
public string Material { get { if (IntValues.ContainsKey(131)) return Dictionaries.MaterialInfo.ContainsKey(IntValues[131]) ? Dictionaries.MaterialInfo[IntValues[131]] : IntValues[131].ToString(CultureInfo.InvariantCulture); return null; } }
public string Name { get { return StringValues.ContainsKey(1) ? StringValues[1] : null; } }
public string EquipSkill { get { if (IntValues.ContainsKey(218103840)) return Dictionaries.SkillInfo.ContainsKey(IntValues[218103840]) ? Dictionaries.SkillInfo[IntValues[218103840]] : IntValues[218103840].ToString(CultureInfo.InvariantCulture); return null; } }
public string Mastery { get { if (IntValues.ContainsKey(353)) return Dictionaries.MasteryInfo.ContainsKey(IntValues[353]) ? Dictionaries.MasteryInfo[IntValues[353]] : IntValues[353].ToString(CultureInfo.InvariantCulture); return null; } }
public string ItemSet { get { if (IntValues.ContainsKey(265)) return Dictionaries.AttributeSetInfo.ContainsKey(IntValues[265]) ? Dictionaries.AttributeSetInfo[IntValues[265]] : IntValues[265].ToString(CultureInfo.InvariantCulture); return null; } }
public int ArmorLevel { get { return IntValues.ContainsKey(28) ? IntValues[28] : -1; } }
public string Imbue
{
get
{
if (!IntValues.ContainsKey(179) || IntValues[179] == 0) return null;
string retVal = String.Empty;
if ((IntValues[179] & 1) == 1) retVal += " CS";
if ((IntValues[179] & 2) == 2) retVal += " CB";
if ((IntValues[179] & 4) == 4) retVal += " AR";
if ((IntValues[179] & 8) == 8) retVal += " SlashRend";
if ((IntValues[179] & 16) == 16) retVal += " PierceRend";
if ((IntValues[179] & 32) == 32) retVal += " BludgeRend";
if ((IntValues[179] & 64) == 64) retVal += " AcidRend";
if ((IntValues[179] & 128) == 128) retVal += " FrostRend";
if ((IntValues[179] & 256) == 256) retVal += " LightRend";
if ((IntValues[179] & 512) == 512) retVal += " FireRend";
if ((IntValues[179] & 1024) == 1024) retVal += " MeleeImbue";
if ((IntValues[179] & 4096) == 4096) retVal += " MagicImbue";
if ((IntValues[179] & 8192) == 8192) retVal += " Hematited";
if ((IntValues[179] & 536870912) == 536870912) retVal += " MagicAbsorb";
retVal = retVal.Trim();
return retVal;
}
}
public int Tinks { get { return IntValues.ContainsKey(171) ? IntValues[171] : -1; } }
public int MaxDamage { get { return IntValues.ContainsKey(218103842) ? IntValues[218103842] : -1; } }
public int ElementalDmgBonus { get { return IntValues.ContainsKey(204) ? IntValues[204] : -1; } }
public Double Variance { get { return DoubleValues.ContainsKey(167772171) ? DoubleValues[167772171] : -1; } }
public Double DamageBonus { get { return DoubleValues.ContainsKey(167772174) ? DoubleValues[167772174] : -1; } }
public Double ElementalDamageVersusMonsters { get { return DoubleValues.ContainsKey(152) ? DoubleValues[152] : -1; } }
public Double AttackBonus { get { return DoubleValues.ContainsKey(167772172) ? DoubleValues[167772172] : -1; } }
public Double MeleeDefenseBonus { get { return DoubleValues.ContainsKey(29) ? DoubleValues[29] : -1; } }
public Double MagicDBonus { get { return DoubleValues.ContainsKey(150) ? DoubleValues[150] : -1; } }
public Double MissileDBonus { get { return DoubleValues.ContainsKey(149) ? DoubleValues[149] : -1; } }
public Double ManaCBonus { get { return DoubleValues.ContainsKey(144) ? DoubleValues[144] : -1; } }
public int WieldLevel { get { if (IntValues.ContainsKey(160) && IntValues[160] > 0 && IntValues.ContainsKey(158) && IntValues[158] == 7 && IntValues.ContainsKey(159) && IntValues[159] == 1) return IntValues[160]; return -1; } }
public int SkillLevel { get { if (IntValues.ContainsKey(160) && IntValues[160] > 0 && (!IntValues.ContainsKey(158) || IntValues[158] != 7) && IntValues.ContainsKey(159)) return IntValues[160]; return -1; } }
public int LoreRequirement { get { return IntValues.ContainsKey(109) ? IntValues[109] : -1; } }
public Double SalvageWorkmanship { get { return DoubleValues.ContainsKey(167772169) ? DoubleValues[167772169] : -1; } }
public int Workmanship { get { return IntValues.ContainsKey(105) ? IntValues[105] : -1; } }
public int Value { get { return IntValues.ContainsKey(19) ? IntValues[19] : -1; } }
public int Burden { get { return IntValues.ContainsKey(5) ? IntValues[5] : -1; } }
public int DamRating { get { return IntValues.ContainsKey(370) ? IntValues[370] : -1; } }
public int DamResistRating { get { return IntValues.ContainsKey(371) ? IntValues[371] : -1; } }
public int CritRating { get { return IntValues.ContainsKey(372) ? IntValues[372] : -1; } }
public int CritResistRating { get { return IntValues.ContainsKey(373) ? IntValues[373] : -1; } }
public int CritDamRating { get { return IntValues.ContainsKey(374) ? IntValues[374] : -1; } }
public int CritDamResistRating { get { return IntValues.ContainsKey(375) ? IntValues[375] : -1; } }
public int HealBoostRating { get { return IntValues.ContainsKey(376) ? IntValues[376] : -1; } }
public int VitalityRating { get { return IntValues.ContainsKey(379) ? IntValues[379] : -1; } }
/// <summary>
/// Returns the sum of all the ratings found on this item, or -1 if no ratings exist.
/// </summary>
public int TotalRating
{
get
{
if (DamRating == -1 && DamResistRating == -1 && CritRating == -1 && CritResistRating == -1 && CritDamRating == -1 && CritDamResistRating == -1 && HealBoostRating == -1 && VitalityRating == -1)
return -1;
return Math.Max(DamRating, 0) + Math.Max(DamResistRating, 0) + Math.Max(CritRating, 0) + Math.Max(CritResistRating, 0) + Math.Max(CritDamRating, 0) + Math.Max(CritDamResistRating, 0) + Math.Max(HealBoostRating, 0) + Math.Max(VitalityRating, 0);
}
}
/// <summary>
/// This will take the current AmorLevel of the item, subtract any buffs, subtract tinks as 20 AL each (not including imbue), and add any impen cantrips.
/// </summary>
public int CalcedStartingArmorLevel
{
get
{
int armorFromTinks = 0;
int armorFromBuffs = 0;
if (Tinks > 0 && ArmorLevel > 0)
armorFromTinks = (Imbue != null) ? (Tinks - 1) * 20 : Tinks * 20; // This assumes each tink adds an amor level of 20
if ((!IntValues.ContainsKey(131) || IntValues[131] == 0) && ArmorLevel > 0) // If this item has no material, its not a loot gen, assume its a quest item and subtract 200 al
armorFromTinks = 200;
foreach (int spell in ActiveSpells)
{
foreach (var effect in Dictionaries.LongValueKeySpellEffects)
{
if (spell == effect.Key && effect.Value.Key == 28)
armorFromBuffs += effect.Value.Change;
}
}
foreach (int spell in Spells)
{
foreach (var effect in Dictionaries.LongValueKeySpellEffects)
{
if (spell == effect.Key && effect.Value.Key == 28)
armorFromBuffs -= effect.Value.Bonus;
}
}
return ArmorLevel - armorFromTinks - armorFromBuffs;
}
}
/// <summary>
/// This will take into account Variance, MaxDamage and Tinks of a melee weapon and determine what its optimal 10 tinked DamageOverTime is.
/// </summary>
public double CalcedBuffedTinkedDoT
{
get
{
if (!DoubleValues.ContainsKey(167772171) || !IntValues.ContainsKey(218103842))
return -1;
double variance = DoubleValues.ContainsKey(167772171) ? DoubleValues[167772171] : 0;
int maxDamage = GetBuffedIntValueKey(218103842);
int numberOfTinksLeft = Math.Max(10 - Math.Max(Tinks, 0), 0);
if (!IntValues.ContainsKey(179) || IntValues[179] == 0)
numberOfTinksLeft--; // Factor in an imbue tink
// If this is not a loot generated item, it can't be tinked
if (!IntValues.ContainsKey(131) || IntValues[131] == 0)
numberOfTinksLeft = 0;
for (int i = 1; i <= numberOfTinksLeft; i++)
{
double ironTinkDoT = CalculateDamageOverTime(maxDamage + 24 + 1, variance);
double graniteTinkDoT = CalculateDamageOverTime(maxDamage + 24, variance * .8);
if (ironTinkDoT >= graniteTinkDoT)
maxDamage++;
else
variance *= .8;
}
return CalculateDamageOverTime(maxDamage + 24, variance);
}
}
/// <summary>
/// GetBuffedIntValueKey(LongValueKey.MaxDamage) + (((GetBuffedDoubleValueKey(DoubleValueKey.DamageBonus) - 1) * 100) / 3) + GetBuffedIntValueKey(LongValueKey.ElementalDmgBonus);
/// </summary>
public double CalcedBuffedMissileDamage { get { if (!IntValues.ContainsKey(218103842) || !DoubleValues.ContainsKey(167772174) || !IntValues.ContainsKey(204)) return -1; return GetBuffedIntValueKey(218103842) + (((GetBuffedDoubleValueKey(167772174) - 1) * 100) / 3) + GetBuffedIntValueKey(204); } }
public double BuffedElementalDamageVersusMonsters { get { return GetBuffedDoubleValueKey(152, -1); } }
public double BuffedAttackBonus { get { return GetBuffedDoubleValueKey(167772172, -1); } }
public double BuffedMeleeDefenseBonus { get { return GetBuffedDoubleValueKey(29, -1); } }
public double BuffedManaCBonus { get { return GetBuffedDoubleValueKey(144, -1); } }
public int GetBuffedIntValueKey(int key, int defaultValue = 0)
{
if (!IntValues.ContainsKey(key))
return defaultValue;
int value = IntValues[key];
foreach (int spell in ActiveSpells)
{
if (Dictionaries.LongValueKeySpellEffects.ContainsKey(spell) && Dictionaries.LongValueKeySpellEffects[spell].Key == key)
value -= Dictionaries.LongValueKeySpellEffects[spell].Change;
}
foreach (int spell in Spells)
{
if (Dictionaries.LongValueKeySpellEffects.ContainsKey(spell) && Dictionaries.LongValueKeySpellEffects[spell].Key == key)
value += Dictionaries.LongValueKeySpellEffects[spell].Bonus;
}
return value;
}
public double GetBuffedDoubleValueKey(int key, double defaultValue = 0)
{
if (!DoubleValues.ContainsKey(key))
return defaultValue;
double value = DoubleValues[key];
foreach (int spell in ActiveSpells)
{
if (Dictionaries.DoubleValueKeySpellEffects.ContainsKey(spell) && Dictionaries.DoubleValueKeySpellEffects[spell].Key == key)
{
if (Math.Abs(Dictionaries.DoubleValueKeySpellEffects[spell].Change - 1) < Double.Epsilon)
value /= Dictionaries.DoubleValueKeySpellEffects[spell].Change;
else
value -= Dictionaries.DoubleValueKeySpellEffects[spell].Change;
}
}
foreach (int spell in Spells)
{
if (Dictionaries.DoubleValueKeySpellEffects.ContainsKey(spell) && Dictionaries.DoubleValueKeySpellEffects[spell].Key == key && Math.Abs(Dictionaries.DoubleValueKeySpellEffects[spell].Bonus - 0) > Double.Epsilon)
{
if (Math.Abs(Dictionaries.DoubleValueKeySpellEffects[spell].Change - 1) < Double.Epsilon)
value *= Dictionaries.DoubleValueKeySpellEffects[spell].Bonus;
else
value += Dictionaries.DoubleValueKeySpellEffects[spell].Bonus;
}
}
return value;
}
/// <summary>
/// maxDamage * ((1 - critChance) * (2 - variance) / 2 + (critChance * critMultiplier));
/// </summary>
/// <param name="maxDamage"></param>
/// <param name="variance"></param>
/// <param name="critChance"></param>
/// <param name="critMultiplier"></param>
/// <returns></returns>
public static double CalculateDamageOverTime(int maxDamage, double variance, double critChance = .1, double critMultiplier = 2)
{
return maxDamage * ((1 - critChance) * (2 - variance) / 2 + (critChance * critMultiplier));
}
public override string ToString()
{
return Name;
}
}
}

View file

@ -0,0 +1,61 @@
using System.Collections.Generic;
using Decal.Adapter.Wrappers;
namespace Mag.Shared
{
public static class MyWorldObjectCreator
{
public static MyWorldObject Create(WorldObject wo)
{
MyWorldObject mwo = new MyWorldObject();
Dictionary<int, bool> boolValues = new Dictionary<int,bool>();
Dictionary<int, double> doubleValues = new Dictionary<int,double>();
Dictionary<int, int> intValues = new Dictionary<int, int>();
Dictionary<int, string> stringValues = new Dictionary<int,string>();
List<int> activeSpells = new List<int>();
List<int> spells = new List<int>();
foreach (var key in wo.BoolKeys)
boolValues.Add(key, wo.Values((BoolValueKey)key));
foreach (var key in wo.DoubleKeys)
doubleValues.Add(key, wo.Values((DoubleValueKey)key));
foreach (var key in wo.LongKeys)
intValues.Add(key, wo.Values((LongValueKey)key));
foreach (var key in wo.StringKeys)
stringValues.Add(key, wo.Values((StringValueKey)key));
for (int i = 0 ; i < wo.ActiveSpellCount ; i++)
activeSpells.Add(wo.ActiveSpell(i));
for (int i = 0; i < wo.SpellCount; i++)
spells.Add(wo.Spell(i));
mwo.Init(wo.HasIdData, wo.Id, wo.LastIdTime, (int)wo.ObjectClass, wo.Icon, boolValues, doubleValues, intValues, stringValues, activeSpells, spells);
return mwo;
}
public static MyWorldObject Combine(MyWorldObject older, WorldObject newer)
{
// Always repair missing icons from old JSON files (plugin upgrades)
if (older.Icon == 0 && newer.Icon != 0)
{
older.Icon = newer.Icon;
}
if (!older.HasIdData || newer.HasIdData)
return Create(newer);
MyWorldObject mwo = Create(newer);
older.AddTo(mwo.BoolValues, mwo.DoubleValues, mwo.IntValues, mwo.StringValues);
return older;
}
}
}

165
Shared/ObjectClass.cs Normal file
View file

@ -0,0 +1,165 @@
using System;
using Mag.Shared.Constants;
namespace Mag.Shared
{
public enum ObjectClass
{
Unknown = 0,
MeleeWeapon = 1,
Armor = 2,
Clothing = 3,
Jewelry = 4,
Monster = 5,
Food = 6,
Money = 7,
Misc = 8,
MissileWeapon = 9,
Container = 10,
Gem = 11,
SpellComponent = 12,
Key = 13,
Portal = 14,
TradeNote = 15,
ManaStone = 16,
Plant = 17,
BaseCooking = 18,
BaseAlchemy = 19,
BaseFletching = 20,
CraftedCooking = 21,
CraftedAlchemy = 22,
CraftedFletching = 23,
Player = 24,
Vendor = 25,
Door = 26,
Corpse = 27,
Lifestone = 28,
HealingKit = 29,
Lockpick = 30,
WandStaffOrb = 31,
Bundle = 32,
Book = 33,
Journal = 34,
Sign = 35,
Housing = 36,
Npc = 37,
Foci = 38,
Salvage = 39,
Ust = 40,
Services = 41,
Scroll = 42,
NumObjectClasses = 43,
}
public static class ObjectClassTools
{
/// <summary>
/// Converts a decal specific IntValueKey to the actual IntValueKey.
/// If this is not an IntValueKey, 0 will be returned.
/// </summary>
public static ObjectClass FromWeenieType(ItemType itemType, WeenieType weenieType)
{
var result = ObjectClass.Unknown;
if ((itemType & ItemType.MeleeWeapon) != 0)
result = ObjectClass.MeleeWeapon;
else if ((itemType & ItemType.Armor) != 0)
result = ObjectClass.Armor;
else if ((itemType & ItemType.Clothing) != 0)
result = ObjectClass.Clothing;
else if ((itemType & ItemType.Jewelry) != 0)
result = ObjectClass.Jewelry;
else if ((itemType & ItemType.Creature) != 0)
result = ObjectClass.Monster;
else if ((itemType & ItemType.Food) != 0)
result = ObjectClass.Food;
else if ((itemType & ItemType.Money) != 0)
result = ObjectClass.Money;
else if ((itemType & ItemType.Misc) != 0)
result = ObjectClass.Misc;
else if ((itemType & ItemType.MissileWeapon) != 0)
result = ObjectClass.MissileWeapon;
else if ((itemType & ItemType.Container) != 0)
result = ObjectClass.Container;
else if ((itemType & ItemType.Useless) != 0)
result = ObjectClass.Bundle;
else if ((itemType & ItemType.Gem) != 0)
result = ObjectClass.Gem;
else if ((itemType & ItemType.SpellComponents) != 0)
result = ObjectClass.SpellComponent;
else if ((itemType & ItemType.Key) != 0)
result = ObjectClass.Key;
else if ((itemType & ItemType.Caster) != 0)
result = ObjectClass.WandStaffOrb;
else if ((itemType & ItemType.Portal) != 0)
result = ObjectClass.Portal;
else if ((itemType & ItemType.PromissoryNote) != 0)
result = ObjectClass.TradeNote;
else if ((itemType & ItemType.ManaStone) != 0)
result = ObjectClass.ManaStone;
else if ((itemType & ItemType.Service) != 0)
result = ObjectClass.Services;
else if ((itemType & ItemType.MagicWieldable) != 0)
result = ObjectClass.Plant;
else if ((itemType & ItemType.CraftCookingBase) != 0)
result = ObjectClass.BaseCooking;
else if ((itemType & ItemType.CraftAlchemyBase) != 0)
result = ObjectClass.BaseAlchemy;
//else if ((itemType & ItemType.01000000)
// result = ObjectClass.BaseFletching;
else if ((itemType & ItemType.CraftFletchingBase) != 0)
result = ObjectClass.CraftedCooking;
else if ((itemType & ItemType.CraftAlchemyIntermediate) != 0)
result = ObjectClass.CraftedAlchemy;
else if ((itemType & ItemType.CraftFletchingIntermediate) != 0)
result = ObjectClass.CraftedFletching;
else if ((itemType & ItemType.TinkeringTool) != 0)
result = ObjectClass.Ust;
else if ((itemType & ItemType.TinkeringMaterial) != 0)
result = ObjectClass.Salvage;
/*
if (Behavior & 0x00000008)
result = ObjectClass.Player;
else if (Behavior & 0x00000200)
result = ObjectClass.Vendor;
else if (Behavior & 0x00001000)
result = ObjectClass.Door;
else if (Behavior & 0x00002000)
result = ObjectClass.Corpse;
else if (Behavior & 0x00004000)
result = ObjectClass.Lifestone;
else if (Behavior & 0x00008000)
result = ObjectClass.Food;
else if (Behavior & 0x00010000)
result = ObjectClass.HealingKit;
else if (Behavior & 0x00020000)
result = ObjectClass.Lockpick;
else if (Behavior & 0x00040000)
result = ObjectClass.Portal;
else if (Behavior & 0x00800000)
result = ObjectClass.Foci;
else if (Behavior & 0x00000001)
result = ObjectClass.Container;
*/
/*if (((itemType & ItemType.Writable) != 0) && (Behavior & 0x00000100) && result == ObjectClass.Unknown)
{
if (pCreate->m_Behavior & 0x00000002)
result = ObjectClass.Journal;
else if (pCreate->m_Behavior & 0x00000004)
result = ObjectClass.Sign;
else if (!(pCreate->m_Behavior & 0x0000000F))
result = ObjectClass.Book;
}*/
/*if (((itemType & ItemType.Writable) != 0) && ((GameDataFlags1 & 0x00400000) != 0))
result = ObjectClass.Scroll;*/
//throw new Exception($"Unable to convert WeenieType {input} to an ObjectClass.");
return result;
}
}
}

293
Shared/PostMessageTools.cs Normal file
View file

@ -0,0 +1,293 @@
using System;
using System.Windows.Forms;
using Decal.Adapter;
namespace Mag.Shared
{
public static class PostMessageTools
{
// http://msdn.microsoft.com/en-us/library/dd375731%28v=vs.85%29.aspx
private const byte VK_RETURN = 0x0D;
private const byte VK_SHIFT = 0x10;
private const byte VK_CONTROL = 0x11;
private const byte VK_PAUSE = 0x13;
private const byte VK_SPACE = 0x20;
private static byte ScanCode(char Char)
{
switch (char.ToLower(Char))
{
case 'a': return 0x1E;
case 'b': return 0x30;
case 'c': return 0x2E;
case 'd': return 0x20;
case 'e': return 0x12;
case 'f': return 0x21;
case 'g': return 0x22;
case 'h': return 0x23;
case 'i': return 0x17;
case 'j': return 0x24;
case 'k': return 0x25;
case 'l': return 0x26;
case 'm': return 0x32;
case 'n': return 0x31;
case 'o': return 0x18;
case 'p': return 0x19;
case 'q': return 0x10;
case 'r': return 0x13;
case 's': return 0x1F;
case 't': return 0x14;
case 'u': return 0x16;
case 'v': return 0x2F;
case 'w': return 0x11;
case 'x': return 0x2D;
case 'y': return 0x15;
case 'z': return 0x2C;
case '/': return 0x35;
case ' ': return 0x39;
}
return 0;
}
private static byte CharCode(char Char)
{
switch (char.ToLower(Char))
{
case 'a': return 0x41;
case 'b': return 0x42;
case 'c': return 0x43;
case 'd': return 0x44;
case 'e': return 0x45;
case 'f': return 0x46;
case 'g': return 0x47;
case 'h': return 0x48;
case 'i': return 0x49;
case 'j': return 0x4A;
case 'k': return 0x4B;
case 'l': return 0x4C;
case 'm': return 0x4D;
case 'n': return 0x4E;
case 'o': return 0x4F;
case 'p': return 0x50;
case 'q': return 0x51;
case 'r': return 0x52;
case 's': return 0x53;
case 't': return 0x54;
case 'u': return 0x55;
case 'v': return 0x56;
case 'w': return 0x57;
case 'x': return 0x58;
case 'y': return 0x59;
case 'z': return 0x5A;
case '/': return 0xBF;
case ' ': return 0x20;
}
return 0x20;
}
public static void SendEnter()
{
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)VK_RETURN, (UIntPtr)0x001C0001);
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)VK_RETURN, (UIntPtr)0xC01C0001);
}
public static void SendPause()
{
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)VK_PAUSE, (UIntPtr)0x00450001);
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)VK_PAUSE, (UIntPtr)0xC0450001);
}
static Timer _spaceReleaseTimer;
static DateTime _spaceSendTime;
static int _spaceHoldTimeMilliseconds;
static bool _spaceAddShift;
static bool _spaceAddW;
static bool _spaceAddZ;
static bool _spaceAddX;
static bool _spaceAddC;
public static void SendSpace(int msToHoldDown = 0, bool addShift = false, bool addW = false, bool addZ = false, bool addX = false, bool addC = false)
{
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)VK_SPACE, (UIntPtr)0x00390001);
if (msToHoldDown == 0)
{
if (addShift) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)VK_SHIFT, (UIntPtr)0x002A0001);
if (addW) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)CharCode('w'), (UIntPtr)0x00110001);
if (addZ) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)CharCode('z'), (UIntPtr)0x002C0001);
if (addX) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)CharCode('x'), (UIntPtr)0x002D0001);
if (addC) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)CharCode('c'), (UIntPtr)0x002E0001);
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)VK_SPACE, (UIntPtr)0xC0390001);
if (addW) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)CharCode('w'), (UIntPtr)0xC0110001);
if (addZ) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)CharCode('z'), (UIntPtr)0xC02C0001);
if (addX) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)CharCode('x'), (UIntPtr)0xC02D0001);
if (addC) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)CharCode('c'), (UIntPtr)0xC02E0001);
if (addShift) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)VK_SHIFT, (UIntPtr)0xC02A0001);
}
else
{
if (_spaceReleaseTimer == null)
{
_spaceReleaseTimer = new Timer();
_spaceReleaseTimer.Tick += new EventHandler(SpaceReleaseTimer_Tick);
_spaceReleaseTimer.Interval = 1;
}
_spaceSendTime = DateTime.UtcNow;
_spaceHoldTimeMilliseconds = msToHoldDown;
_spaceAddShift = addShift;
_spaceAddW = addW;
_spaceAddZ = addZ;
_spaceAddX = addX;
_spaceAddC = addC;
_spaceReleaseTimer.Start();
}
}
static void SpaceReleaseTimer_Tick(object sender, EventArgs e)
{
if (_spaceSendTime.AddMilliseconds(_spaceHoldTimeMilliseconds) <= DateTime.UtcNow)
{
_spaceReleaseTimer.Stop();
if (_spaceAddShift) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)VK_SHIFT, (UIntPtr)0x002A0001);
if (_spaceAddW) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)CharCode('w'), (UIntPtr)0x00110001);
if (_spaceAddZ) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)CharCode('z'), (UIntPtr)0x002C0001);
if (_spaceAddX) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)CharCode('x'), (UIntPtr)0x002D0001);
if (_spaceAddC) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)CharCode('c'), (UIntPtr)0x002E0001);
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)VK_SPACE, (UIntPtr)0xC0390001);
if (_spaceAddW) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)CharCode('w'), (UIntPtr)0xC0110001);
if (_spaceAddZ) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)CharCode('z'), (UIntPtr)0xC02C0001);
if (_spaceAddX) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)CharCode('x'), (UIntPtr)0xC02D0001);
if (_spaceAddC) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)CharCode('c'), (UIntPtr)0xC02E0001);
if (_spaceAddShift) User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)VK_SHIFT, (UIntPtr)0xC02A0001);
}
}
static Timer _movementReleaseTimer;
static DateTime _movementSendTime;
static int _movementHoldTimeMilliseconds;
static char _movementKey;
public static void SendMovement(char ch, int msToHoldDown = 0)
{
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)CharCode(ch), (UIntPtr)(0x00000001 + ScanCode(ch) * 0x10000));
if (msToHoldDown == 0)
{
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)CharCode(ch), (UIntPtr)(0xC0000001 + ScanCode(ch) * 0x10000));
}
else
{
if (_movementReleaseTimer == null)
{
_movementReleaseTimer = new Timer();
_movementReleaseTimer.Tick += new EventHandler(MovementReleaseTimer_Tick);
_movementReleaseTimer.Interval = 1;
}
_movementSendTime = DateTime.Now;
_movementHoldTimeMilliseconds = msToHoldDown;
_movementKey = ch;
_movementReleaseTimer.Start();
}
}
static void MovementReleaseTimer_Tick(object sender, EventArgs e)
{
if (_movementSendTime.AddMilliseconds(_movementHoldTimeMilliseconds) <= DateTime.Now)
{
_movementReleaseTimer.Stop();
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)CharCode(_movementKey), (UIntPtr)(0xC0000001 + ScanCode(_movementKey) * 0x10000));
}
}
public static void SendCntrl(char ch)
{
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)VK_CONTROL, (UIntPtr)0x001D0001);
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)CharCode(ch), (UIntPtr)0x00100001);
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)CharCode(ch), (UIntPtr)0xC0100001);
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)VK_CONTROL, (UIntPtr)0xC01D0001);
}
public static void SendAltF4()
{
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_DESTROY, new IntPtr(0), new UIntPtr(0));
}
/// <summary>
/// Opens/Closes fellowship view
/// </summary>
public static void SendF4()
{
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)0x00000073, (UIntPtr)0x003E0001);
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)0x00000073, (UIntPtr)0xC03E0001);
}
/// <summary>
/// Opens/Closes main pack view
/// </summary>
public static void SendF12()
{
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)0x0000007B, (UIntPtr)0x00580001);
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)0x0000007B, (UIntPtr)0xC0580001);
}
public static void SendMsg(string msg)
{
foreach (char ch in msg)
{
byte code = CharCode(ch);
uint lparam = (uint)((ScanCode(ch) << 0x10) | 1);
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYDOWN, (IntPtr)code, (UIntPtr)(lparam));
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_KEYUP, (IntPtr)code, (UIntPtr)(0xC0000000 | lparam));
}
}
public static void ClickOK()
{
User32.RECT rect = new User32.RECT();
User32.GetWindowRect(CoreManager.Current.Decal.Hwnd, ref rect);
// The reason why we click at both of these positions is some clients will be running windowed, and some windowless. This will hit both locations
SendMouseClick(rect.Width / 2, rect.Height / 2 + 18);
SendMouseClick(rect.Width / 2, rect.Height / 2 + 25);
SendMouseClick(rect.Width / 2, rect.Height / 2 + 31);
}
public static void ClickYes()
{
User32.RECT rect = new User32.RECT();
User32.GetWindowRect(CoreManager.Current.Decal.Hwnd, ref rect);
// 800x600 +32 works, +33 does not work on single/double/tripple line boxes
// 1600x1200 +31 works, +32 does not work on single/double/tripple line boxes
// The reason why we click at both of these positions is some clients will be running windowed, and some windowless. This will hit both locations
SendMouseClick(rect.Width / 2 - 80, rect.Height / 2 + 18);
SendMouseClick(rect.Width / 2 - 80, rect.Height / 2 + 25);
SendMouseClick(rect.Width / 2 - 80, rect.Height / 2 + 31);
}
public static void ClickNo()
{
User32.RECT rect = new User32.RECT();
User32.GetWindowRect(CoreManager.Current.Decal.Hwnd, ref rect);
// The reason why we click at both of these positions is some clients will be running windowed, and some windowless. This will hit both locations
SendMouseClick(rect.Width / 2 + 80, rect.Height / 2 + 18);
SendMouseClick(rect.Width / 2 + 80, rect.Height / 2 + 25);
SendMouseClick(rect.Width / 2 + 80, rect.Height / 2 + 31);
}
public static void SendMouseClick(int x, int y)
{
int loc = (y * 0x10000) + x;
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_MOUSEMOVE, (IntPtr)0x00000000, (UIntPtr)loc);
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_LBUTTONDOWN, (IntPtr)0x00000001, (UIntPtr)loc);
User32.PostMessage(CoreManager.Current.Decal.Hwnd, User32.WM_LBUTTONUP, (IntPtr)0x00000000, (UIntPtr)loc);
}
}
}

59
Shared/RateLimiter.cs Normal file
View file

@ -0,0 +1,59 @@
using System;
using System.Diagnostics;
namespace Mag.Shared
{
public class RateLimiter
{
public readonly int MaxNumberOfEvents;
private readonly double overPeriodInSeconds;
private readonly double targetEventSpacingInSeconds;
private readonly Stopwatch stopwatch = Stopwatch.StartNew();
public RateLimiter(int maxNumberOfEvents, TimeSpan overPeriod)
{
if (maxNumberOfEvents <= 0)
throw new ArgumentOutOfRangeException(nameof(maxNumberOfEvents), $"{nameof(maxNumberOfEvents)} must be greater than 0");
if (overPeriod <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(overPeriod), $"{nameof(overPeriod)} must be greater than TimeSpan.Zero");
MaxNumberOfEvents = maxNumberOfEvents;
overPeriodInSeconds = overPeriod.TotalSeconds;
targetEventSpacingInSeconds = overPeriodInSeconds / maxNumberOfEvents;
}
private int numberOfEventsRegistered;
/// <summary>
/// Result > 0 : We're able to meet our target rate and must pause between events.<para />
/// Result = 0 : We're running right no time. A new event should be registered without delay.<para />
/// Result &lt; 0 : We're running behind. A new event should be registered without delay. We're failing to meet our target rate.
/// </summary>
/// <returns></returns>
public double GetSecondsToWaitBeforeNextEvent()
{
var elapsedSeconds = stopwatch.Elapsed.TotalSeconds;
return ((targetEventSpacingInSeconds * numberOfEventsRegistered) - elapsedSeconds);
}
public void RegisterEvent()
{
numberOfEventsRegistered++;
var elapsedSeconds = stopwatch.Elapsed.TotalSeconds;
if (numberOfEventsRegistered > MaxNumberOfEvents || elapsedSeconds > overPeriodInSeconds)
{
numberOfEventsRegistered = 1;
stopwatch.Reset();
stopwatch.Start();
}
}
}
}

View file

@ -0,0 +1,81 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace Mag.Shared
{
[XmlRoot("dictionary")]
public class SerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IXmlSerializable
{
public System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
public void ReadXml(System.Xml.XmlReader reader)
{
XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));
bool wasEmpty = reader.IsEmptyElement;
reader.Read();
if (wasEmpty)
return;
while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
{
reader.ReadStartElement("item");
reader.ReadStartElement("key");
TKey key = (TKey)keySerializer.Deserialize(reader);
reader.ReadEndElement();
reader.ReadStartElement("value");
TValue value = (TValue)valueSerializer.Deserialize(reader);
reader.ReadEndElement();
this.Add(key, value);
reader.ReadEndElement();
reader.MoveToContent();
}
reader.ReadEndElement();
}
public void WriteXml(System.Xml.XmlWriter writer)
{
XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));
foreach (TKey key in this.Keys)
{
writer.WriteStartElement("item");
writer.WriteStartElement("key");
keySerializer.Serialize(writer, key);
writer.WriteEndElement();
writer.WriteStartElement("value");
TValue value = this[key];
valueSerializer.Serialize(writer, value);
writer.WriteEndElement();
writer.WriteEndElement();
}
}
}
}

View file

@ -0,0 +1,59 @@
using System;
namespace Mag.Shared.Settings
{
class Setting<T>
{
public readonly string Xpath;
public readonly string Description;
public readonly T DefaultValue;
private T value;
public T Value
{
get
{
return value;
}
set
{
// If we're setting it to the value its already at, don't continue with the set.
if (Object.Equals(this.value, value))
return;
// The value differs, set it.
this.value = value;
StoreValueInConfigFile();
if (Changed != null)
Changed(this);
}
}
public event Action<Setting<T>> Changed;
public Setting(string xpath, string description = null, T defaultValue = default(T))
{
Xpath = xpath;
Description = description;
DefaultValue = defaultValue;
LoadValueFromConfig(defaultValue);
}
void LoadValueFromConfig(T defaultValue)
{
value = SettingsFile.GetSetting(Xpath, defaultValue);
}
void StoreValueInConfigFile()
{
SettingsFile.PutSetting(Xpath, value);
}
}
}

View file

@ -0,0 +1,210 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Xml;
namespace Mag.Shared.Settings
{
static class SettingsFile
{
internal static readonly XmlDocument XmlDocument = new XmlDocument();
static string _documentPath;
static string _rootNodeName = "Settings";
static SettingsFile()
{
ReloadXmlDocument();
}
public static void Init(string filePath, string rootNode = "Settings")
{
_documentPath = filePath;
_rootNodeName = rootNode;
ReloadXmlDocument();
}
public static void ReloadXmlDocument()
{
try
{
if (!String.IsNullOrEmpty(_documentPath) && File.Exists(_documentPath))
XmlDocument.Load(_documentPath);
else
XmlDocument.LoadXml("<" + _rootNodeName + "></" + _rootNodeName + ">");
}
catch (Exception ex)
{
Debug.LogException(ex);
XmlDocument.LoadXml("<" + _rootNodeName + "></" + _rootNodeName + ">");
}
}
public static void SaveXmlDocument()
{
XmlDocument.Save(_documentPath);
}
public static T GetSetting<T>(string xPath, T defaultValue = default(T))
{
XmlNode xmlNode = XmlDocument.SelectSingleNode(_rootNodeName + "/" + xPath);
if (xmlNode != null)
{
TypeConverter converter = TypeDescriptor.GetConverter(typeof(T));
if (converter.CanConvertFrom(typeof(string)))
return (T)converter.ConvertFromString(xmlNode.InnerText);
}
return defaultValue;
}
public static void PutSetting<T>(string xPath, T value)
{
// Before we save a setting, we reload the document to make sure we don't overwrite settings saved from another session.
ReloadXmlDocument();
XmlNode xmlNode = XmlDocument.SelectSingleNode(_rootNodeName + "/" + xPath);
if (xmlNode == null)
xmlNode = createMissingNode(_rootNodeName + "/" + xPath);
TypeConverter converter = TypeDescriptor.GetConverter(typeof(T));
if (converter.CanConvertTo(typeof(string)))
{
string result = converter.ConvertToString(value);
if (result != null)
{
xmlNode.InnerText = result;
XmlDocument.Save(_documentPath);
}
}
}
static XmlNode createMissingNode(string xPath)
{
string[] xPathSections = xPath.Split('/');
string currentXPath = "";
XmlNode currentNode = XmlDocument.SelectSingleNode(_rootNodeName);
foreach (string xPathSection in xPathSections)
{
currentXPath += xPathSection;
XmlNode testNode = XmlDocument.SelectSingleNode(currentXPath);
if (testNode == null)
{
if (currentNode != null)
currentNode.InnerXml += "<" + xPathSection + "></" + xPathSection + ">";
}
currentNode = XmlDocument.SelectSingleNode(currentXPath);
currentXPath += "/";
}
return currentNode;
}
public static IList<string> GetChilderenInnerTexts(string xPath)
{
XmlNode xmlNode = XmlDocument.SelectSingleNode(_rootNodeName + "/" + xPath);
Collection<string> collection = new Collection<string>();
if (xmlNode != null)
{
foreach (XmlNode childNode in xmlNode.ChildNodes)
collection.Add(childNode.InnerText);
}
return collection;
}
public static void SetNodeChilderen(string xPath, string childNodeName, IList<string> innerTexts)
{
// Before we save a setting, we reload the document to make sure we don't overwrite settings saved from another session.
ReloadXmlDocument();
XmlNode parentNode = XmlDocument.SelectSingleNode(_rootNodeName + "/" + xPath);
if (parentNode == null)
{
if (innerTexts.Count == 0)
return;
parentNode = createMissingNode(_rootNodeName + "/" + xPath);
}
parentNode.RemoveAll();
if (innerTexts.Count == 0)
{
XmlDocument.Save(_documentPath);
return;
}
foreach (string innerText in innerTexts)
{
XmlNode childNode = parentNode.AppendChild(XmlDocument.CreateElement(childNodeName));
childNode.InnerText = innerText;
}
XmlDocument.Save(_documentPath);
}
public static XmlNode GetNode(string xPath, bool createIfNull = false)
{
var node = XmlDocument.SelectSingleNode(_rootNodeName + "/" + xPath);
if (node == null && createIfNull)
node = createMissingNode(_rootNodeName + "/" + xPath);
return node;
}
public static void SetNodeChilderen(string xPath, string childNodeName, Collection<Dictionary<string, string>> childNodeAttributes)
{
// Before we save a setting, we reload the document to make sure we don't overwrite settings saved from another session.
ReloadXmlDocument();
XmlNode parentNode = XmlDocument.SelectSingleNode(_rootNodeName + "/" + xPath);
if (parentNode == null)
parentNode = createMissingNode(_rootNodeName + "/" + xPath);
if (parentNode.HasChildNodes)
parentNode.RemoveAll();
foreach (Dictionary<string, string> dictionary in childNodeAttributes)
{
XmlNode childNode = parentNode.AppendChild(XmlDocument.CreateElement(childNodeName));
foreach (KeyValuePair<string, string> pair in dictionary)
{
XmlAttribute attribute = XmlDocument.CreateAttribute(pair.Key);
attribute.Value = pair.Value;
if (childNode.Attributes != null)
childNode.Attributes.Append(attribute);
}
}
XmlDocument.Save(_documentPath);
}
}
}

View file

@ -0,0 +1,100 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
namespace Mag.Shared
{
class SortableBindingList<T> : BindingList<T> where T : class
{
protected override bool SupportsSortingCore { get { return true; } }
private bool isSorted;
private ListSortDirection sortDirection;
private PropertyDescriptor sortProperty;
protected override bool IsSortedCore { get { return isSorted; } }
protected override ListSortDirection SortDirectionCore { get { return sortDirection; } }
protected override PropertyDescriptor SortPropertyCore { get { return sortProperty; } }
protected override void ApplySortCore(PropertyDescriptor prop, ListSortDirection direction)
{
sortProperty = prop;
sortDirection = direction;
List<T> items = (List<T>)Items;
items.Sort(delegate(T lhs, T rhs)
{
isSorted = true;
object lhsValue = lhs == null ? null : prop.GetValue(lhs);
object rhsValue = rhs == null ? null : prop.GetValue(rhs);
int result = Comparer.Default.Compare(lhsValue, rhsValue);
if (direction == ListSortDirection.Descending)
result = -result;
return result;
});
}
protected override void RemoveSortCore()
{
sortDirection = ListSortDirection.Ascending;
sortProperty = null;
}
/// <summary>
/// Sorts using the default IComparer of T
/// </summary>
public void Sort()
{
sort(null, null);
}
public void Sort(IComparer<T> comparer)
{
sort(comparer, null);
}
public void Sort(Comparison<T> comparison)
{
sort(null, comparison);
}
private void sort(IComparer<T> comparer, Comparison<T> comparison)
{
sortProperty = null;
sortDirection = ListSortDirection.Ascending;
//Extract items and sort separately
List<T> sortList = new List<T>();
foreach (var item in this)
sortList.Add(item);
if (comparison == null)
sortList.Sort(comparer);
else
sortList.Sort(comparison);
//Disable notifications, rebuild, and re-enable notifications
bool oldRaise = RaiseListChangedEvents;
RaiseListChangedEvents = false;
try
{
ClearItems();
sortList.ForEach(item => Add(item));
}
finally
{
RaiseListChangedEvents = oldRaise;
ResetBindings();
}
}
}
}

188
Shared/Spells/Spell.cs Normal file
View file

@ -0,0 +1,188 @@

namespace Mag.Shared.Spells
{
/// <summary>
/// Use GetSpell() to intialize a Spell object
/// </summary>
public class Spell
{
public readonly int Id;
public readonly string Name;
public readonly int Difficulty;
public readonly int Duration;
public readonly int Family;
public enum BuffLevels
{
None,
I,
II,
III,
IV,
V,
VI,
VII,
VIII,
}
public readonly BuffLevels BuffLevel;
public enum CantripLevels
{
None,
Feeble,
Minor,
Lesser,
Moderate,
Inner,
Major,
Epic,
Legendary,
}
public readonly CantripLevels CantripLevel;
public Spell(int id, string name, int difficulty, int duration, int family)
{
Id = id;
Name = name;
Difficulty = difficulty;
Duration = duration;
Family = family;
if (name.EndsWith(" I"))
BuffLevel = BuffLevels.I;
else if (name.EndsWith(" II"))
BuffLevel = BuffLevels.II;
else if (name.EndsWith(" III"))
BuffLevel = BuffLevels.III;
else if (name.EndsWith(" IV"))
BuffLevel = BuffLevels.IV;
else if (name.EndsWith(" V"))
BuffLevel = BuffLevels.V;
else if (name.EndsWith(" VI"))
BuffLevel = BuffLevels.VI;
else if (name.EndsWith(" VII")) // This doesn't pick up every lvl 7
BuffLevel = BuffLevels.VII;
else if (name.StartsWith("Incantation ") || name.StartsWith("Aura of Incantation "))
BuffLevel = BuffLevels.VIII;
if (name.StartsWith("Feeble "))
CantripLevel = CantripLevels.Feeble;
else if (name.StartsWith("Minor "))
CantripLevel = CantripLevels.Minor;
else if (name.StartsWith("Lesser "))
CantripLevel = CantripLevels.Lesser;
else if (name.StartsWith("Moderate "))
CantripLevel = CantripLevels.Moderate;
else if (name.StartsWith("Inner ") && name != "Inner Calm")
CantripLevel = CantripLevels.Inner;
else if (name.StartsWith("Major "))
CantripLevel = CantripLevels.Major;
else if (name.StartsWith("Epic "))
CantripLevel = CantripLevels.Epic;
else if (name.StartsWith("Legendary "))
CantripLevel = CantripLevels.Legendary;
// Try to determine if this is a lvl x
if (BuffLevel == BuffLevels.None && CantripLevel == CantripLevels.None)
{
// These spells don't have levels
if (name.StartsWith("Prodigal ") || // Rares
name.StartsWith("Cloaked in ") || name.StartsWith("Shroud of ")) // Cloaks
return;
if (difficulty == 1 && duration == 1800)
BuffLevel = BuffLevels.I;
if (difficulty == 50 && duration == -1 && name.StartsWith("Evaporate "))
BuffLevel = BuffLevels.II;
if (difficulty == 50 && duration == 1800)
BuffLevel = BuffLevels.II;
if (difficulty == 100 && duration == -1 && name.StartsWith("Extinguish "))
BuffLevel = BuffLevels.II;
if (difficulty == 100 && duration == 1800)
BuffLevel = BuffLevels.III;
if (difficulty == 150 && duration == -1 && name.StartsWith("Cleanse "))
BuffLevel = BuffLevels.III;
if (difficulty == 150 && duration == 1800)
BuffLevel = BuffLevels.IV;
if (difficulty == 200 && duration == -1 && name.StartsWith("Devour "))
BuffLevel = BuffLevels.IV;
if (difficulty == 200 && duration == 1800)
BuffLevel = BuffLevels.V;
if (difficulty == 250 && duration == -1 && name.StartsWith("Purge "))
BuffLevel = BuffLevels.V;
if (difficulty == 200 && duration == -1 && !name.StartsWith("Devour ")) // Ring spells
BuffLevel = BuffLevels.VI;
if (difficulty == 250 && duration == 2700)
BuffLevel = BuffLevels.VI;
if (difficulty == 300 && duration == -1 && name.StartsWith("Nullify "))
BuffLevel = BuffLevels.VI;
if (difficulty == 300 && duration == -1 && !name.StartsWith("Nullify "))
BuffLevel = BuffLevels.VII;
if (difficulty == 300 && duration == 240)
BuffLevel = BuffLevels.VII;
if (difficulty == 300 && duration == 3600)
BuffLevel = BuffLevels.VII;
if (difficulty == 325 && duration == -1)
BuffLevel = BuffLevels.VII;
if (difficulty == 325 && duration == 240)
BuffLevel = BuffLevels.VII;
if (difficulty == 350 && duration == -1)
BuffLevel = BuffLevels.VII;
if (difficulty == 400 && duration == 5400)
BuffLevel = BuffLevels.VIII;
}
}
public bool IsOfSameFamilyAndGroup(Spell compareSpell)
{
if (Family != compareSpell.Family)
return false;
if (BuffLevel != 0 && compareSpell.BuffLevel != 0)
return true;
if (CantripLevel != 0 && compareSpell.CantripLevel != 0)
return true;
// Are both spells are of an unkown group?
if (BuffLevel == 0 && compareSpell.BuffLevel == 0 && CantripLevel == 0 && compareSpell.CantripLevel == 0)
return true;
return false;
}
public bool IsSameOrSurpasses(Spell compareSpell)
{
return (this == compareSpell || Surpasses(compareSpell));
}
public bool Surpasses(Spell compareSpell)
{
if (Family == 0 || Family != compareSpell.Family)
return false;
if (BuffLevel > 0 && compareSpell.BuffLevel > 0 && BuffLevel > compareSpell.BuffLevel)
return true;
if (CantripLevel > 0 && compareSpell.CantripLevel > 0 && CantripLevel > compareSpell.CantripLevel)
return true;
return false;
}
public override string ToString()
{
return Name;
}
}
}

149
Shared/Spells/SpellTools.cs Normal file
View file

@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Reflection;
namespace Mag.Shared.Spells
{
public static class SpellTools
{
static readonly List<string> SpellTableHeader = new List<string>();
static readonly Collection<string[]> SpellTable = new Collection<string[]>();
static readonly Dictionary<int, Spell> SpellsById = new Dictionary<int, Spell>();
static SpellTools()
{
var resourceNames = Assembly.GetExecutingAssembly().GetManifestResourceNames();
if (resourceNames.Length == 0)
return;
var rootNameSpace = resourceNames[0].Substring(0, resourceNames[0].IndexOf('.'));
using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(rootNameSpace + ".Shared.Spells.Spells.csv"))
{
if (stream != null)
{
using (StreamReader reader = new StreamReader(stream))
{
SpellTableHeader = new List<string>(reader.ReadLine().Split(','));
while (!reader.EndOfStream)
SpellTable.Add(reader.ReadLine().Split(','));
}
}
}
}
/// <summary>
/// Will return null if no spell was found.
/// </summary>
public static Spell GetSpell(int id)
{
if (SpellsById.TryGetValue(id, out var spell))
return spell;
int idIndex = SpellTableHeader.IndexOf("Id");
foreach (string[] line in SpellTable)
{
if (line[idIndex] == id.ToString(CultureInfo.InvariantCulture))
return GetSpell(line);
}
//throw new ArgumentException("Spell of id: " + id + " not found in Spells.csv");
return null;
}
/// <summary>
/// Will return null if no spell was found.
/// </summary>
public static Spell GetSpell(string name)
{
foreach (var kvp in SpellsById)
{
if (String.Equals(kvp.Value.Name, name, StringComparison.OrdinalIgnoreCase))
return kvp.Value;
}
int nameIndex = SpellTableHeader.IndexOf("Name");
foreach (string[] line in SpellTable)
{
if (String.Equals(line[nameIndex], name, StringComparison.OrdinalIgnoreCase))
return GetSpell(line);
}
//throw new ArgumentException("Spell of name: " + name + " not found in Spells.csv");
return null;
}
private static Spell GetSpell(string[] splitLine)
{
int idIndex = SpellTableHeader.IndexOf("Id");
int nameIndex = SpellTableHeader.IndexOf("Name");
int difficultyIndex = SpellTableHeader.IndexOf("Difficulty");
int durationIndex = SpellTableHeader.IndexOf("Duration");
int familyIndex = SpellTableHeader.IndexOf("Family");
int id;
int.TryParse(splitLine[idIndex], out id);
string name = splitLine[nameIndex];
int difficulty;
int.TryParse(splitLine[difficultyIndex], out difficulty);
int duration;
int.TryParse(splitLine[durationIndex], out duration);
int family;
int.TryParse(splitLine[familyIndex], out family);
var spell = new Spell(id, name, difficulty, duration, family);
if (!SpellsById.ContainsKey(spell.Id))
SpellsById.Add(spell.Id, spell);
return spell;
}
public static bool IsAKnownSpell(int id)
{
if (SpellsById.ContainsKey(id))
return true;
int idIndex = SpellTableHeader.IndexOf("Id");
foreach (string[] line in SpellTable)
{
if (line[idIndex] == id.ToString(CultureInfo.InvariantCulture))
return true;
}
return false;
}
public static bool IsAKnownSpell(string name)
{
foreach (var kvp in SpellsById)
{
if (String.Equals(kvp.Value.Name, name, StringComparison.OrdinalIgnoreCase))
return true;
}
int nameIndex = SpellTableHeader.IndexOf("Name");
foreach (string[] line in SpellTable)
{
if (line[nameIndex] == name)
return true;
}
return false;
}
}
}

6267
Shared/Spells/Spells.csv Normal file

File diff suppressed because it is too large Load diff

81
Shared/User32.cs Normal file
View file

@ -0,0 +1,81 @@
using System;
using System.Runtime.InteropServices;
namespace Mag.Shared
{
public static class User32
{
public const int WM_ACTIVATE = 0x0006;
public const int WM_SETFOCUS = 0x0007;
public const int WM_KILLFOCUS = 0x0008;
public const int WM_ACTIVATEAPP = 0x001C;
public const int WM_DESTROY = 0x0002;
public const int WM_KEYDOWN = 0x0100;
public const int WM_KEYUP = 0x0101;
public const int WM_CHAR = 0x0102;
public const int WM_MOUSEMOVE = 0x0200;
public const int WM_LBUTTONDOWN = 0x0201;
public const int WM_LBUTTONUP = 0x0202;
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern bool PostMessage(IntPtr hhwnd, uint msg, IntPtr wparam, UIntPtr lparam);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetCursorPos(out System.Drawing.Point lpPoint);
[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);
//Gets window attributes
[DllImport("user32.dll", SetLastError = true)]
public static extern int GetWindowLong(IntPtr hWnd, int nIndex);
//Sets window attributes
/// <summary>
/// Changes an attribute of the specified window. The function also sets the 32-bit (long) value at the specified offset into the extra window memory.
/// </summary>
/// <param name="hWnd">A handle to the window and, indirectly, the class to which the window belongs..</param>
/// <param name="nIndex">The zero-based offset to the value to be set. Valid values are in the range zero through the number of bytes of extra window memory, minus the size of an integer. To set any other value, specify one of the following values: GWL_EXSTYLE, GWL_HINSTANCE, GWL_ID, GWL_STYLE, GWL_USERDATA, GWL_WNDPROC </param>
/// <param name="dwNewLong">The replacement value.</param>
/// <returns>If the function succeeds, the return value is the previous value of the specified 32-bit integer.
/// If the function fails, the return value is zero. To get extended error information, call GetLastError. </returns>
[DllImport("user32.dll")]
public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
/// <summary>
/// The MoveWindow function changes the position and dimensions of the specified window. For a top-level window, the position and dimensions are relative to the upper-left corner of the screen. For a child window, they are relative to the upper-left corner of the parent window's client area.
/// </summary>
/// <param name="hWnd">Handle to the window.</param>
/// <param name="x">Specifies the new position of the left side of the window.</param>
/// <param name="y">Specifies the new position of the top of the window.</param>
/// <param name="nWidth">Specifies the new width of the window.</param>
/// <param name="nHeight">Specifies the new height of the window.</param>
/// <param name="bRepaint">Specifies whether the window is to be repainted. If this parameter is TRUE, the window receives a message. If the parameter is FALSE, no repainting of any kind occurs. This applies to the client area, the nonclient area (including the title bar and scroll bars), and any part of the parent window uncovered as a result of moving a child window.</param>
/// <returns>If the function succeeds, the return value is nonzero.
/// <para>If the function fails, the return value is zero. To get extended error information, call GetLastError.</para></returns>
[DllImport("user32.dll", SetLastError = true)]
public static extern bool MoveWindow(IntPtr hWnd, int x, int y, int nWidth, int nHeight, bool bRepaint);
public const int SW_MINIMIZE = 6;
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
}
}

Some files were not shown because too many files have changed in this diff Show more