using System; using System.Reflection; using Advanced_Combat_Tracker; using System.Windows.Forms; using System.Text.RegularExpressions; using System.Text; using System.Globalization; using System.Collections.Generic; [assembly: AssemblyTitle("STO Parsing Plugin for PVP")] [assembly: AssemblyDescription("A basic parser that reads the combat logs in Star Trek Online.")] [assembly: AssemblyCopyright("Hilbert@mancom, Pirye@ucalegon")] [assembly: AssemblyVersion("2.0.3.0")] /* Version History * 2.0.3.0 - 2017/12/09 * - Hide unnecessary columns * 2.0.2.0 - 2014/05/04 * - Fixed an issue with heals being detected as damage due to SA-changes introducing raw/net heals * 2.0.1.3 - 2013/04/13 * - Fixed an issue with wrong NPC/pet detection related to Ramming Speed * 2.0.1.2 - 2013/01/22 * - Fixed an issue with pet/owner relationship detection that primarily occured in STFs * 2.0.1.1 - 2012/12/15 * - Fixed an issue with shield damage from pets * 2.0.1.0 - 2012/09/29 * - Fixed some problems with radiation/electrical/hargh'peng damage and pet detection * 2.0.0.1 - 2012/09/08 * - Fixed an issue that made some kills show up incorrectly * 2.0.0.0 - 2012/09/06 * - Changed everything back to [Pet Spam] and made pets a part of the owner's damage again (with name prefix) ============================== * 1.0.6.2 - 2012/08/16 * - Added delayed parsing by log blocks (lines with same time) to identify missing pet owners for first pet shot against shield * - known issues: * - some lines at the end may not be parsed unless you add ",EOF" at the end of last combat line. this can't be fixed unless ACT adds an end of file flag on the line parsing delegate, or an end of file event. * the issue only happens when in the last log block (lines with same time) there are pet shield shots for which no pet owner can be found * - STO writes some log lines with missing fields, they will be reported with the attacker, target or type being "unknown". This is most likely due to the owner dying 10s or more before the action, and is hard to fix. * example without owner or source: 12:08:15:03:34:49.0::,,,,Nanite Sphere,C[89850 Mission_Infected_Healer_Sphere],Transphasic Torpedo,Pn.Usr4f31,Kinetic,,2325.64,1857.05 * 1.0.6.1 - 2012/08/08 * - prevented shield attack lines from misleading the parser into thinking targets were pets * - added pet-owner dictionary to attribute most pet shield damage to owner * - known issues: * - pet shield damage before it first hits hull isn't attributed to pet owner, requires log buffering to fix. * 1.0.6.0 - 2012/07/13 * - adapted parser to Season 6 combat logs (parseable shield damage), better pet damage processing * 1.0.5.2 - 2012/06/01 * - made warp core breaches part of another category (threat) to avoid influencing DPS * 1.0.5.1 - 2012/05/29 * - added special attack parsing, added some missing attacks (vent plasma, last scatter shot, ...), made targets more identifiable than [pet spam] ============================== * 1.0.5.0 - 2012/04/17 * - resolved a problem with misses and pets * 1.0.4.3 - 2012/04/07 * - pet deaths' not counting anymore * 1.0.4.2 - 2012/03/24 * - pet deaths counting again, to measure level of pet spam in matches * 1.0.4.0 - 2011/12/17 * - Borg radiation proc was counting as 0 * 1.0.3.0 - 2011/11/27 * - pets' deaths were counting towards players' kills * 1.0.0.0 - 2011/10/?? * - initial version */ namespace Parsing_Plugin { public class STO_Parser : IActPluginV1 { internal static string[] separatorLog = new string[] { "::", "," }; internal static string unk = "[Pet Spam]", unkInt = "C[ Unknown]", pet = " ", unkAbility = "Unknown Ability"; internal static CultureInfo cultureLog = new CultureInfo("en-US"), cultureDisplay = new CultureInfo("de-DE"); // Pet internal id to owner display name internal static Dictionary petCache = new Dictionary(); // Instant when the current combat action took place private DateTime curActionTime = DateTime.MinValue; // Lines of the current action private List delayedActions = new List(20); Label lblStatus = null; public void InitPlugin(TabPage pluginScreenSpace, Label pluginStatusText) { // Setting this Regex will allow ACT to extract the character's name from the file name as the first capture group // when opening new log files. We'll say the log file name may look like "20080706-Player.log" ActGlobals.oFormActMain.LogPathHasCharName = false; // A windows file system filter to search updated log files with. ActGlobals.oFormActMain.LogFileFilter = "Combat*.log"; // If all log files are in a single folder, this isn't an issue. If log files are split into different folders, // enter the parent folder name here. This way ACT will monitor that base folder and all sub-folders for updated files. ActGlobals.oFormActMain.LogFileParentFolderName = "GameClient"; // Then to apply the settings and restart the log checking thread try { ActGlobals.oFormActMain.ResetCheckLogs(); } catch { // Ignore when no log file is currently open } // This is the absolute path of where you wish ACT generated macro exports to be put. I'll leave it up to you // to determine this path programatically. If left blank, ACT will attempt to find EQ2 by registry or log file parents. // ActGlobals.oFormActMain.GameMacroFolder = @"C:\Program Files\Game Company\Game Folder"; // Lets say that the log file time stamp is like: "[13:42:57]" // ACT needs to know the length of the timestamp and spacing at the beginning of the log line ActGlobals.oFormActMain.TimeStampLen = 19; // Remember to include spaces after the time stamp // Replace ACT's default DateTime parser with your own implementation matching your format ActGlobals.oFormActMain.GetDateTimeFromLog = new FormActMain.DateTimeLogParser(ParseDateTime); // This Regex is only used by a quick parsing method to find the current zone name based on a file position // If you do not define this correctly, the quick parser will fail and take a while to do so. // You still need to implement a zone change parser in your engine regardless of this // ActGlobals.oFormActMain.ZoneChangeRegex = new Regex(@"You have entered: (.+)\.", RegexOptions.Compiled); // All of your parsing engine will be based off of this event // You should not use Before/AfterCombatAction as you may enter infinite loops. AfterLogLineRead is okay, but not recommended ActGlobals.oFormActMain.BeforeLogLineRead += new LogLineEventDelegate(oFormActMain_BeforeLogLineRead); // Hooks for periodic pet cache purge ActGlobals.oFormActMain.OnCombatEnd += new CombatToggleEventDelegate(oFormActMain_OnCombatEnd); ActGlobals.oFormActMain.LogFileChanged += new LogFileChangedDelegate(oFormActMain_LogFileChanged); try { CombatantData.OutgoingDamageTypeDataObjects.Remove("Power Drain (Out)"); CombatantData.OutgoingDamageTypeDataObjects.Remove("Power Replenish (Out)"); CombatantData.OutgoingDamageTypeDataObjects.Remove("Cure/Dispel (Out)"); CombatantData.OutgoingDamageTypeDataObjects.Remove("Threat (Out)"); CombatantData.IncomingDamageTypeDataObjects.Remove("Power Drain (Inc)"); CombatantData.IncomingDamageTypeDataObjects.Remove("Power Replenish (Inc)"); CombatantData.IncomingDamageTypeDataObjects.Remove("Cure/Dispel (Inc)"); CombatantData.IncomingDamageTypeDataObjects.Remove("Threat (Inc)"); } catch(Exception ex) { MessageBox.Show(ex.ToString()); } lblStatus = pluginStatusText; lblStatus.Text = "STO ACT plugin loaded"; } void oFormActMain_LogFileChanged(bool IsImport, string NewLogFileName) { curActionTime = DateTime.MinValue; purgePetCache(); } void oFormActMain_OnCombatEnd(bool isImport, CombatToggleEventArgs encounterInfo) { curActionTime = DateTime.MinValue; purgePetCache(); } private void purgePetCache() { petCache.Clear(); } // Must match LogLineEventDelegate signature void oFormActMain_BeforeLogLineRead(bool isImport, LogLineEventArgs logInfo) { if (logInfo.logLine.Length < 30 || logInfo.logLine[19] != ':' || logInfo.logLine[20] != ':') { // Not a valid log line, parse remaining actions since this might be EOF parseDelayedActions(true); return; } if (logInfo.detectedTime != curActionTime) { // Different times mean new action block, any pending actions won't be related to those of the new block parseDelayedActions(true); curActionTime = logInfo.detectedTime; } ParsedLine pl = new ParsedLine(logInfo); delayedActions.Add(pl); if (pl.eof) { // Line has EOF marker, force parsing of all delayed lines parseDelayedActions(true); } else if (!pl.shouldDelayParse()) { parseDelayedActions(false); } } private void parseDelayedActions(bool parseUnmatched) { int i = 0; while (i < delayedActions.Count) { ParsedLine l = delayedActions[i]; bool parsed = false, matched = true; bool isPetAction = l.srcInt.CompareTo(l.ownInt) != 0 && !l.npcOwner && !l.npcTarget; //&& !l.npcOwner && l.npcTarget; // For attacks absorbed by Shields, the owner is the actual damage source, the logged source is the attack target. // We need to know which player spawned the pet in order to record his indirect damage, but we won't know that until the pet hits hull. // So for now all pet shield damage before first hull hit is attributed to the pet, which sucks for mines since they only have one shot. string realSrcInt = l.srcInt, realSrcDsp = l.srcDsp, realOwnDsp = l.ownDsp; // Auch Radiation Damage und so kann Probleme machen bool ignorePet = l.type.Equals("Radiation") || l.type.Equals("Electrical") || l.attackType.Equals("Hargh'Peng Torpedo Secondary Detonation") || l.attackType.Contains("Ramming Speed"); if (!realSrcInt.Equals(unkInt)) { if (!ignorePet && isPetAction) { // When a player pet hits the hull of something, store pet-player relationship for later lookups if (!petCache.ContainsKey(realSrcInt)) { petCache.Add(realSrcInt, realOwnDsp); } } if (ignorePet || l.npcOwner) { // Potential pet shield attack, try to find actual owner if (petCache.ContainsKey(l.ownInt)) { realSrcInt = l.ownInt; realSrcDsp = l.ownDsp; realOwnDsp = petCache[l.ownInt]; ignorePet = false; isPetAction = true; } else { // Owner yet unknown. We look ahead in current action lines to see if we have a matching hull damage to guess owner bool found = false; for (int j = i+1; j < delayedActions.Count; j++) { ParsedLine p = delayedActions[j]; if (!p.npcOwner && p.srcInt == l.ownInt && p.evtInt == l.evtInt && p.tgtInt == l.tgtInt) { // Match when owner is a player, same event type, same target, and match source is equal to current line owner realSrcInt = l.ownInt; realSrcDsp = l.ownDsp; realOwnDsp = p.ownDsp; ignorePet = false; isPetAction = true; petCache.Add(realSrcInt, p.ownDsp); found = true; break; } } if (!found) { matched = false; } } } } if (matched || parseUnmatched) { if (l.mag >= 0) { // Basic attack, magnitude is actual damage dealt taking resists/buffs/debuffs/critical into account, magnitudeBase is damage without these addAttack(l, (int)Math.Round(l.mag), isPetAction, realSrcDsp, realOwnDsp, ignorePet); } else if (l.mag < 0 && (l.magBase == 0 || l.type == "HitPoints")) // HullHealing hat jetzt Raw/Net-Werte; Schildheilung aber irgendwie noch nicht { string tempTarget = l.tgtDsp; string tempOwner = l.ownDsp; if (l.npcTarget) tempTarget = STO_Parser.unk; // pet targets abfangen if (l.npcOwner) tempOwner = STO_Parser.unk; // Heals have base magnitude 0 and negative magnitude string tempAttack = l.attackType; if ((l.srcDsp != l.ownDsp) && (l.type=="HitPoints" || l.type=="Shield")) tempAttack = l.srcDsp + ": " + l.attackType; addCombatAction(l.logInfo.detectedTime, tempOwner, tempTarget, (int)SwingTypeEnum.Healing, l.critical, l.special, tempAttack, new Dnum((int)Math.Round(-l.mag)), l.type, l.ts); } else if (l.mag < 0 && l.magBase < 0) { // Attack absorbed by shields, magnitude is shield damage, magnitudebase is potential hull damage the shield protected the target from addAttack(l, (int)Math.Round(-l.mag), isPetAction, realSrcDsp, realOwnDsp, ignorePet); } parsed = true; } if (parsed) { delayedActions.RemoveAt(i); } else { i++; } } if (parseUnmatched) { delayedActions.Clear(); } } private static void addAttack(ParsedLine l, int dmg, bool isPetAction, string realSrcDsp, string realOwnDsp, bool ignorePet) { string tempTarget = l.tgtDsp; string tempOwner = realOwnDsp; if (l.npcTarget) tempTarget = STO_Parser.unk; // pet targets abfangen if (l.swingType==(int)SwingTypeEnum.NonMelee) tempOwner = realOwnDsp; if (l.npcOwner && realOwnDsp==l.ownDsp) tempOwner = STO_Parser.unk; if (realSrcDsp == unk && realOwnDsp!=unk) return; if (l.flags.Contains("Miss") || l.flags.Contains("Dodge")) { if (l.swingType==(int)SwingTypeEnum.NonMelee) { //(!ignorePet && isPetAction) { // Pet miss, doppelt um Hitrate zu korrigieren // Hargh'Pengs (auch Hargh'peng geschrieben) nicht mit Source prefixen string tempAttack = l.attackType; if (!l.attackType.Contains("Hargh")) tempAttack = realSrcDsp + ": " + l.attackType; addCombatAction(l.logInfo.detectedTime, tempOwner, tempTarget, l.swingType, l.critical, l.special, tempAttack, new Dnum(dmg), l.type, l.ts); addCombatAction(l.logInfo.detectedTime, tempOwner, tempTarget, l.swingType, l.critical, l.special, tempAttack, new Dnum(dmg), l.type, l.ts); //addCombatAction(l.logInfo.detectedTime, realSrcDsp, l.tgtDsp, l.swingType, l.critical, l.special, l.attackType, Dnum.Miss, l.type, l.ts); } else { // Normal miss, doppelt um Hitrate zu korrigieren addCombatAction(l.logInfo.detectedTime, tempOwner, tempTarget, l.swingType, l.critical, l.special, l.attackType, Dnum.Miss, l.type, l.ts); addCombatAction(l.logInfo.detectedTime, tempOwner, tempTarget, l.swingType, l.critical, l.special, l.attackType, Dnum.Miss, l.type, l.ts); } } else { if (l.swingType==(int)SwingTypeEnum.NonMelee) { //(!ignorePet && isPetAction) { // Collect pet damage // Hargh'Pengs (auch Hargh'peng geschrieben) nicht mit Source prefixen string tempAttack = l.attackType; if (!l.attackType.Contains("Hargh") && !l.attackType.Equals("Aceton Assimilator") && !l.attackType.Equals("Isometric Charge") && !realSrcDsp.Equals("Aceton Assimilator")) tempAttack = realSrcDsp + ": " + l.attackType; addCombatAction(l.logInfo.detectedTime, tempOwner, tempTarget, l.swingType, l.critical, l.special, tempAttack, new Dnum(dmg), l.type, l.ts); //addCombatAction(l.logInfo.detectedTime, realSrcDsp, l.tgtDsp, l.swingType, l.critical, l.special, l.attackType, new Dnum(dmg), l.type, l.ts); } else { addCombatAction(l.logInfo.detectedTime, tempOwner, tempTarget, l.swingType, l.critical, l.special, l.attackType, new Dnum(dmg), l.type, l.ts); } } if (l.flags.Contains("Kill") && !l.npcTarget) { addCombatAction(l.logInfo.detectedTime, tempOwner, tempTarget, l.swingType, l.critical, l.special, "Killing", Dnum.Death, l.type, l.ts); } } private static void addCombatAction(DateTime Time, string attacker, string victim, int swingType, bool critical, string special, string theAttackType, Dnum Damage, string theDamageType, int ts) { if (ActGlobals.oFormActMain.SetEncounter(Time, attacker, victim)) { string tempAttack = theAttackType; if (special!="None" && theAttackType!="Killing") tempAttack = theAttackType + ": " + special; ActGlobals.oFormActMain.AddCombatAction(swingType, critical, special, attacker, tempAttack, Damage, Time, ts, victim, theDamageType); } } // Must match the DateTimeLogParser delegate signature private DateTime ParseDateTime(string FullLogLine) { if (FullLogLine.Length >= 21 && FullLogLine[19] == ':' && FullLogLine[20] == ':') { return DateTime.ParseExact(FullLogLine.Substring(0, 19), "yy':'MM':'dd':'HH':'mm':'ss'.'f", System.Globalization.CultureInfo.InvariantCulture); ; } return ActGlobals.oFormActMain.LastKnownTime; } public void DeInitPlugin() { ActGlobals.oFormActMain.BeforeLogLineRead -= oFormActMain_BeforeLogLineRead; purgePetCache(); lblStatus.Text = "STO ACT plugin unloaded"; } } // Pre-parsed line internal class ParsedLine { public LogLineEventArgs logInfo; public String ownDsp, ownInt, srcDsp, srcInt, tgtDsp, tgtInt, evtDsp, evtInt; public String type, attackType, special, flags; public int swingType, ts; public bool critical, npcOwner, npcSource, npcTarget, eof; public float mag, magBase; public ParsedLine(LogLineEventArgs logInfo) { this.logInfo = logInfo; this.ts = ++ActGlobals.oFormActMain.GlobalTimeSorter; string[] split = logInfo.logLine.Split(STO_Parser.separatorLog, StringSplitOptions.None); ownDsp = split[1]; ownInt = split[2]; srcDsp = split[3]; srcInt = split[4]; tgtDsp = split[5]; tgtInt = split[6]; evtDsp = split[7]; evtInt = split[8]; type = split[9]; flags = split[10]; mag = float.Parse(split[11], STO_Parser.cultureLog); magBase = float.Parse(split[12], STO_Parser.cultureLog); if (split.Length > 13 && split[13].CompareTo("EOF") == 0) { // Ugly patch for last lines parsing, since there's no way to trigger parsing at end of file without NREs in ACT this.eof = true; } if (ownDsp == "" && ownInt == "") { // Ugly fix for lines without an owner ownDsp = STO_Parser.unk; ownInt = STO_Parser.unkInt; } if (srcDsp == "") { if (srcInt == "*") { srcDsp = ownDsp; srcInt = ownInt; } else if (srcInt == "") { // Ugly fix for lines without a source srcDsp = STO_Parser.unk; srcInt = STO_Parser.unkInt; } } if (tgtDsp == "") { if (tgtInt == "*") { tgtDsp = srcDsp; tgtInt = srcInt; } else if (tgtInt == "") { // Ugly fix for lines without a target tgtDsp = STO_Parser.unk; tgtInt = STO_Parser.unkInt; } } npcOwner = ownInt[0] != 'P'; npcSource = srcInt[0] != 'P'; npcTarget = tgtInt[0] != 'P'; // Qualify display names with internal instance number /* if (npcOwner && ownDsp != STO_Parser.unk) { ownDsp = ownDsp; + " #" + ownInt.Substring(2, ownInt.IndexOf(" ") - 2); } if (npcSource && srcDsp != STO_Parser.unk) { srcDsp = srcDsp; + " #" + srcInt.Substring(2, srcInt.IndexOf(" ") - 2); } if (npcTarget && tgtDsp != STO_Parser.unk) { tgtDsp = tgtDsp; + " #" + tgtInt.Substring(2, tgtInt.IndexOf(" ") - 2); } */ if (flags.Contains("Critical")) { critical = true; } swingType = (int)SwingTypeEnum.Melee; // Das war früher "&&" - geändert um mit Schild-Damage umzugehen if (npcOwner || npcSource) { swingType = (int)SwingTypeEnum.NonMelee; } special = "None"; attackType = evtDsp; if (attackType.Trim().Length == 0) { // Uggly fix for missing attack type attackType = STO_Parser.unkAbility; } int pos = attackType.IndexOf(" - "); if (pos > 0) { special = attackType.Substring(pos + 3, attackType.Length - pos - 3); attackType = attackType.Substring(0, pos); } if (attackType.Contains("Warp Core Breach")) { // Classify WCBs as non melee attacks to avoid interpreting affected allies as foes swingType = (int)SwingTypeEnum.NonMelee; } if (npcOwner || (npcSource && (mag>0))) { //Pets als nonMelee anzeigen swingType = (int)SwingTypeEnum.NonMelee; } } internal bool shouldDelayParse() { // Currently we only need to delay parse shield damage when the damage owner is a pet whose owner is unknown return "Shield".Equals(type) && mag < 0 && magBase < 0 && npcOwner && !STO_Parser.petCache.ContainsKey(ownInt); } } }