Follow along with the video below to see how to install our site as a web app on your home screen.
Note: This feature may not be available in some browsers.

Commits
(4a1e89a) ~John KauffmanLatest
- Drop the vendored Piper binaries, the pack is their only home now
(40318cf) ~John KauffmanEQLogParser\piper-tts held five native DLLs -- piperApi, piper_phonemize, espeak-ng and the onnxruntime pair -- plus a README. Deleted: 13 MB that nothing in the product reads any more, kept alive by a workflow that runs once per binary bump.
The folder predates runtime packs, when these files were installed into {app}\piper-tts and were the app. Since then Piper reached the user as a download and the folder stayed behind with a narrower job: the csproj copied it to the build output so sign.cmd had something at %RELEASE_DIR%\piper-tts to sign, and scripts\Build-TtsPack.ps1 -Sync could pass those signed copies to the pack staging tree. Checked each consumer rather than trusting that comment:
- Run time reads packs. The Piper entry in TtsPackManager.Packs pins piper-1.0.zip by SHA-256 from a GitHub release, unpacked into %LOCALAPPDATA%\EQLogParser\piper-tts, and NativeSearchDirectories() yields installed pack directories only. A piper-tts under the program folder is ignored on purpose -- which is what stops an engine running off files the TTS Engine dialog can neither update nor remove.
- The build does not see them. piperApi.dll is P/Invoked by name at run time, never referenced by MSBuild; the proof is that this tree builds the whole solution on Linux, where those DLLs are inert files.
- The installer has no [Files] entry for it, and never did since packs landed.
- -Sync only ever copied them if present, so the pack flow was already tolerant of their absence.
Only signing needed them, and signing does not need them to live here: whatever gets staged is what gets signed and zipped. So a Piper binary bump now starts in the TTS data repo's staging tree -- copy the DLLs out of a published pack, or build them from the Piper fork (upstream rhasspy/piper; nothing in this repo carries the source) -- sign them there, then pack as usual. -Inventory lists any missing file and says so in the same words, and an unsigned piperApi.dll warns while packing where -Strict would refuse it. docs/TtsPacks.md has the longer version.
What changed to keep that honest:
Build-TtsPack.ps1 stops pretending a build contributes anything Piper-side. Sync-FromBuild drops the $PiperBin loop along with the espeak-ng-data and voice-folder syncs that existed for checkouts old enough to still carry data; -Sync now contributes exactly what an app build really produces -- Kokoro's support assemblies, its native ONNX Runtime from runtimes\win-x64\native, and the .npy embeddings. Sync-Tree lost its last caller and went, taking Copy-IfChanged's -Quiet with it. Header layout, -Sync's own description and the missing-file messages say where these files come from now, because "piper-tts/piperApi.dll is missing" is not a helpful error if the reader assumes an app build would have provided it.
sign.cmd drops the five piper-tts targets and records why they are absent, so nobody re-adds them hunting a warning. The Kokoro pack files are still signed where the build puts them, and the rule is unchanged: sign before manifest, vendor signatures left intact rather than replaced with ours.
The csproj None Include="piper-tts\**" item goes with the folder. Worth one manual step from anyone with an existing checkout: MSBuild does not undo a removed item, so bin\...**\piper-tts is still sitting in build outputs on this machine (deleted here). Nothing reads it, but a hand-zipped pack could pick up a stale DLL from it, which is exactly the failure mode packs exist to prevent.
- Claim onnxruntime.dll instead of hoping resolution picks ours
(0472765) ~John KauffmanKokoro can fail on a machine while its download is fine. The graph is refused by somebody else's runtime: other programs drop onnxruntime.dll into C:\Windows\System32, a 1.7-era copy answers DllImport("onnxruntime") from there, and the error says "Unsupported model IR version" about a model that loaded correctly elsewhere. Two properties of Windows make that fatal rather than untidy. Native modules are keyed by base name for the life of the process, so whoever maps onnxruntime.dll first holds it, and a later load by absolute path to a different file hands back the resident module. And the default search -- deps.json native assets, then LoadLibraryEx over the usual directories including System32 -- runs before .NET asks any resolver, so both TtsPackManager hooks, which are only consulted after that search fails, cannot take part. The resolvers registered for Microsoft.ML.OnnxRuntime never fire either.
It also explains why this appeared on a VM and not on the machine chasing it: EQLogParser.deps.json declares runtimes/win-x64/native/onnxruntime.dll, the build output has the file, and the installer deliberately does not ship it -- that is ~12MB of a ~20MB installer when the packs carry it. A declared native asset that is not on disk is simply not found, so a clean install falls through to the operating system while a development run resolves ours correctly.
PreferMatchingOnnxRuntime() is now that decision in one place, and it claims rather than resolves: load EQLP's own copy by absolute path so the name is ours from then on. Candidate order is <kokoro>\native then <piper-tts>; Kokoro leads because its pack publishes the runtime together with the managed wrapper installed beside the executable and is repacked whenever that wrapper moves, while Piper's carries the same build and either serves. Both engines were already calling it, which is fine but was the wrong owner -- a Kokoro-only session reached ONNX through code that never asked the question -- so AudioManager claims once on the engine-ready task before TtsEngineFactory.Create(), off the UI thread because mapping 12MB is not startup work, and both engines keep their calls to cover a pack downloaded mid-session. Once it succeeds.
Three supports, each covering what the claim cannot:
Confirming rather than assuming. An absolute-path load returns the already-resident module, so TryLoad succeeding does not prove the file asked for is in use. IsForeignOnnxRuntimeResident() asks Process.Modules which path actually holds the name and rejects anything outside the packs and the program folder; Kokoro then refuses to start with a message naming that path and version, because "re-download your 156MB model" aimed at a healthy file is a waste of an afternoon and a pack re-download. Log.Error says out loud not to delete things from Windows: the name is not recoverable until the process ends, what fixes it is removing whatever installed the other copy.
Pinning the wrapper's own imports (EnsureOnnxRuntimeImportResolver) so P/Invoke cannot drift to another copy -- second line only, since an import resolver runs after the default search. Its real job is the mirror failure: EQLP's runtime missing throws instead of returning IntPtr.Zero, because handing the name back is precisely how System32 gets in. Fail loudly on a decision we own.
WarnOnRuntimeDrift compares the mapped module against the wrapper installed beside the executable, warn-not-refuse; major.minor disagreement means one of the two moved alone, which per docs/TtsPacks.md should not happen.
onnxruntime.dll also imports the MSVC runtime -- msvcp140, msvcp140_1, vcruntime140, vcruntime140_1 -- and EQLogParser had been assuming the redistributable was on every machine. The four now ship in EQLogParser\redist, Microsoft-signed and left that way, installed flat into {app}, claimed by name in the same call before ONNX Runtime is mapped. Before rather than after is the point: a runtime loaded from %LOCALAPPDATA%\EQLogParser\kokoro\native gets an altered search path -- its own directory then the system directories -- so the program folder is not in that list and four DLLs beside EQLogParser.exe would do nothing for it unless those names were already resident.
Placement has a consequence stated plainly in redist\README.md rather than glossed: the search order puts the program folder ahead of System32, so these are the copies this process uses even where the machine has a newer redistributable. That is Microsoft's local deployment of the CRT and it holds under one duty -- keep the checked-in copies current, since the CRT serves older binaries forward and an up-to-date app-local copy is a safe floor for every native library in the process. Refreshing means all four together; ClaimVisualCppRuntimes logs which file answered each name, as does MeasureLoadedAssemblies.
sign.cmd routes them through SignPackFile so a Microsoft signature survives rather than being replaced with ours.
Not included: shipping the full native ONNX Runtime in the installer. It duplicates what the packs carry for ~12MB, and claiming the pack copy is deterministic once the claim happens before either engine touches ONNX. bottles/eqlogparser.yml keeps its vcredist2022 dependency -- app-local covers the speech engines' CRT, not a fresh prefix's other needs, and dropping it wants a test nobody has run.
Tests cover the parts testable without a live process: candidate order puts Kokoro first when both packs are present, IsOwnedNativePath treats pack and program paths as ours and System32 as not -- with the trailing-separator comparison that stops EQLogParser-old from counting -- and IsOnnxRuntimeLibrary accepting only the runtime's own name. EQLogParser.Wpf.Test needs Microsoft.WindowsDesktop.App and so still needs a Windows run; the claim itself is only observable there, and what to look for in the log is in docs/ReleaseChecklist.md.
(c3f932f) ~John KauffmanLatest
Bug Fixes
(52c3582) ~John KauffmanFix version
Documentation
- Say what a Wine prefix needs before installing EQLogParser
(79f9756) ~John KauffmanThe Linux section only mentioned the .NET Desktop Runtime, and never
described how to install at all. Added an Installing with Bottles and an
Installing with plain Wine path, both of which put vcredist2022 in the
prefix - without it Piper and Kokoro download their files and then stay
silent on "Unable to load DLL 'onnxruntime'". Known Issues gained that
symptom with its fix, a note that the pack itself verified fine so no
re-download is needed, and where the several hundred MB land.
- Record the version bump steps and what InstallDelete really frees
(4d894c2) ~John KauffmanThe checklist covered signing and the installer file lists but not the
release numbers, which is how README's download link slips a version.
Added the list of places a version moves, with the build.py step that
regenerates the installer's release-notes RTF.
Corrected two pack claims while there: packs land in
%LOCALAPPDATA%\EQLogParser\<engine>-<version> (piper-1.0), not <engine>,
and "[InstallDelete] clears roughly 150MB" was wrong in both directions -
an ordinary install has nothing to reclaim, a pipertts install gives back
several hundred MB.
- Describe the timer name option as Dynamic, on by default
(a4f5e69) ~John KauffmanThe Trigger Manager checkbox is "Dynamic Timer Name" and is checked by
default since it was reworded; triggers.md still documented the old,
inverted "Static Timer Name" and told people to check it to pin a name.
Rewritten for the control as shipped: what checked does, that unchecking
pins the captured value for one run, and that unchecking also keeps a
{variable} that vanishes from showing raw on the timer bar.
- Write down what the visual studio cleanup enforces
(3820c3d) ~John KauffmanThe cleanup pass left style rules in the code that the standards doc
never had - and one it contradicted. Point the doc at .editorconfig as
the source of truth, list the non-obvious enforced choices (no space
after casts, var for built-in types and out args, static local
functions, parentheses for clarity, collection expressions), and fix
the stale brace line: every declaration in the codebase is Allman,
which csharp_new_line_before_open_brace = all enforces.
- Where the first utterance delay comes from and what warms it
(0b8787e) ~John Kauffman
- Explain the browse/Use split behind Remove Files
(96c2565) ~John Kauffman
- Write down that packs are the only speech path
(b8ee638) ~John KauffmanFour places described the app-local Piper copy as still honored. They describe one location per engine now, and why a directory the app quietly adopts is worse than a re-download. The piper-tts README also says its ONNX copies have to stay the version Kokoro ships, because Windows holds one onnxruntime.dll per process.
- Record why the ntdll probe is trustworthy
(d1df2b0) ~John KauffmanThe asymmetry argument, the System32 pinning and which wrappers turn out
to be Wine underneath, so nobody has to rederive any of it when this
check one day looks redundant.
- Write down how an engine becomes available
(c3fa348) ~John KauffmanThe rules are none of them guessable from the call sites: Windows
availability is a verdict recorded by LoadVoicesAsync, unknown counts as
available because only the started engine gets probed, both WinRT and
legacy SAPI have to come back empty before it is called false, and an
engine with no voices is refused as a switch target rather than switched
to. Plus the log line that turns "no audio" bug reports into something
diagnosable, and what the picker greys out versus why a pack engine stays
clickable.
- Describe the packs the app now installs
(7f382c2) ~John KauffmanThe status sections said the loader was still to come; it is here, so
the notes moved from intention to mechanism: the pin table with the tag
and archive digest of each published pack, the install sequence and why
each step exists (digest before extraction, manifest after, staging
directory, marker file), which resolver answers what, and the two
version-skew traps that will bite on a future dependency bump — the
managed ONNX wrapper installs with the app while its native half comes
from the pack, and KokoroSharp runs on top of everything in the pack.
Piper's data no longer appears anywhere in this repo, so -Sync stopped
claiming it can supply espeak-ng-data and the Piper voices; it now finds
the Kokoro graph in an installed pack first. KokoroVoicePrefixes was
documented under a name it does not have.
Testing
- Expect the region, not the language, for pt_BR
(4610bda) ~John KauffmanRegionOf has always returned what comes after the underscore - that is
the region, per its own doc comment (zh_CN reads CN) and every sibling
assertion in this file. The one pt_BR line expected the language code
instead, so it failed on first run on a machine that can execute the
Wpf suite.
- Pin what an unknown spell does in the counts pipeline
(3712521) ~John KauffmanGround truth for why EverQuest Legends casts never reached the spell
count table. An unknown name is one that parses on a cast line but is
not in the bundled spell data: the record keeps an IsUnknown stub, the
stub stays out of the real DB, and later casts reuse it.
Pinned now, with the real pipeline (RecordsStore -> QuerySpells ->
GetSpellCounts): an unknown cast during a fight counts exactly like any
non-damaging spell, so neither the core pipeline nor the default
category filters drop it. What can drop it: a cast outside the window
around every fight, and a spell whose casts were all interrupted, which
sits at zero under the Any Frequency floor.
Commits
(beb0cd5) ~John KauffmanRemoved comments
- Stop deleting app-local speech files on upgrade
(679ceec) ~John KauffmanThe [InstallDelete] additions from commit # [ea7a46c](https://github.com/kauffman12/EQLogParser/commit/ea7a46c6) aimed at a state that never shipped. Six of the eight names (MisakiSharp, NumSharp, OpenTK x3, System.Numerics.Tensors) plus {app}\voices only ever existed in setups built from commit # [27213ac](https://github.com/kauffman12/EQLogParser/commit/27213ac2), the branch commit that bundled Kokoro for a day before the pack design replaced it: master has no KokoroSharp package reference and no release tag installs any of those files. No user can have them.
{app}\piper-tts is real — every release through 2.3.61 lays it down and 2.3.62 stops reading it — but users put their own Piper voices in {app}\piper-tts\voices, so leaving a few hundred inert MB beats deleting models they added. The uninstaller still clears {app}.
Section goes back to what it was: libraries dropped years ago and superseded docs under {app}\data.
- Require vcredist2022 so the speech engines can speak
(4cfdf21) ~John KauffmanThe Piper and Kokoro packs carry Microsoft's onnxruntime, which imports
msvcp140, msvcp140_1, vcruntime140 and vcruntime140_1 - none of them
supplied by Wine. A bottle without the redistributable downloads a pack
that verifies cleanly and then fails with "Unable to load DLL
'onnxruntime' ... Module not found", which is what a user reported for
Kokoro. Listed first because dotnetdesktop8 sits on the same runtime.
Bottles' own vcredist2022 installer is the right verb: it installs both
arches of VC_redist and overrides those dll names as native,builtin.
- Order voice pickers by the label they show
(c991ed4) ~John KauffmanKokoro sorted its ids, which leads with accent and gender: af_nicole comes
before am_adam, so every American woman preceded Adam and "Yunxi (CN)"
filed itself between the Hindi and Japanese voices. Piper did not sort at
all and listed whatever order the pack's voices.json happened to be built
in. Both now go through TtsVoiceOrder, which orders by the label the engine
prints and keeps the id as a tie break so equal labels do not trade places
between calls.
Sorting is display only; the stored setting stays the engine's id. The
Windows list is left alone - it leads with the system default voice on
purpose, and Windows names its voices the way a person already would.
- Point the README download link at 2.3.62
(540eeb2) ~John KauffmanIt carried the previous version through the bump, so the top download link
on the repo front page offered the old installer. Goes live with the tag.
- Fix the wording of the 2.3.62 note
(355a31f) ~John KauffmanTwo typos ("descritpions", "povided") in the entry the installer shows and
the feed publishes, and the Linux line now says to see above for where the
engine picker is instead of leaving people to find the Tools dropdown.
RTF regenerated with build.py so InfoBeforeFile matches.
(6f6e055) ~John KauffmanUpdated version and release notes
- Stop warning about a saved engine whose name differs only in case
(61ad654) ~John KauffmanPlanHint compared this window's normalized engine name against the raw
TtsEngine setting, so a hand-edited "piper" produced "the saved choice,
piper, would not start" while Piper was speaking. Compare without regard
to case and cover it in the action button tests.
- Log the voice label failure, mute repeated speak-path lines
(72a4568) ~John KauffmanVoiceNameConverter held the solution's last Debug.WriteLine. Logging is a
single RollingFileAppender with no Debug appender, so it reached nowhere a
user could hand over as a bug report; it now goes to Log.Debug with the
exception.
Piper and Kokoro raised Warn from paths that repeat by construction: an
empty synthesis once per callout, a voice load failure once per player
binding it, and the Kokoro cross-directory notice on every voice lookup.
The first occurrence still reaches Warn - it carries the whole message -
and the repeats drop to Debug, the way MarkUnavailable and the Wine probe
already behave.
- Cover the TTS engine options in the FAQ, trim the Linux section
(b43c155) ~John KauffmanNew FAQ question for the three speech engines - Windows (built-in
voices, nothing to download, absent under Wine), Piper (fast and
lightweight, lowest voice quality) and Kokoro (most natural, most
demanding) - with where to switch (Trigger Manager, Tools > TTS Engine),
how the Download/Use buttons behave, live switching, voices belonging
to their engine, and the packs living in %LOCALAPPDATA% (Remove Files).
The old Piper TTS section is no longer valid: the app does not read a
speech runtime from beside the executable anymore, TtsPackManager
downloads and verifies packs into local app data, and the installer
deletes legacy piper-tts copies. It is replaced by the FAQ entry, which
also tells anyone following old instructions that the Google Drive zip
and manual unzip are gone.
Linux is a core feature of the main installer now, so the Flatpak/
Bottles and manual wine install guides come out. What stays: a short
intro (same installer under 64-bit Wine) and the still-true known
issues; the WINE x64 item that Windows TTS does not work there now
points at the new FAQ entry instead of a manual Piper install.
"Callouts" is raid jargon, so the docs say triggers speak text out
loud, the index Linux CTA drops "installation guide", and getting
started links the new FAQ entry.
- Offer the timer name option as Dynamic, on by default
(ded2b8b) ~John KauffmanNobody has the old version yet, so instead of a migration the flag is
simply inverted: TimerNameDynamic defaults to true (the name follows
variable changes, which was always the default behaviour) and gets
unchecked to freeze the name at timer start. The runtime TimerData flag
renames with it - DynamicDisplayName, also true by default - and the
copy test now seeds the new non-default value.
(fec6395) ~John KauffmanUpdated tooltips and descriptions plus visual studio code cleanup
- Call the trigger helper dropdown Tools, not Settings
(6ad847d) ~John KauffmanSettings is not a word this app uses - Options means preferences and
Tools means helper windows you open to do something, which is exactly
what Dictionary, Quick Shares and TTS Engine are. Name it after the
category it matches and give the button a tooltip saying what it opens.
(30df4f5) ~John KauffmanMerge branch 'master' into integrate-pr-406
- Tuck the three settings buttons behind one dropdown
(c0c5e71) ~John KauffmanThe TTS Engine, Dictionary and Quick Shares buttons on the Triggers
toolbar were each a plain dialog opener with no state of their own, so
they become the items of one Settings dropdown in the house
DropDownButtonAdv pattern (Manage Layouts, New): no icon, tighter
padding, alphabetical. Each item keeps its old tooltip and reuses the
old Click handlers, so only the XAML and the DesignNotes pointer move.
- Build the first engine off the UI thread and keep piper speaking
(6c040ca) ~John KauffmanA Kokoro session over the 156 MB model graph takes seconds, and the
constructor ran it on whatever thread touched Instance first - the UI
thread at startup. The build now runs on the thread pool and
LoadValidVoicesAsync is the single point that waits for it.
A Piper player whose voice failed to load used to speak through an id
piper never received a model for: no audio, while GetVoice still
reported the default. It now speaks the default instead.
A Piper preference falls through to an installed Kokoro before the
Windows voices (TtsEngineFactory.FallbackOrder, now unit tested).
Stale onnxruntime version comment, misplaced locale comment, a few
brace-style slips and DesignNotes get tidied while we are here.
- Stop a half removed pack from looking like a choice
(671b7af) ~John KauffmanThree reports and one chain of them, with the dialog knowing less than it could each
time. Removing an engine used earlier in the session answered "restart first", which
is correct - that runtime keeps its libraries mapped until EQLogParser closes - but
not before Directory.Delete had walked partway through the tree. What remained was a
directory holding some of a runtime: too broken to speak with, not absent either, so
the next start offered Download Piper and Remove Files side by side under a line
saying Piper was not installed. Both buttons were right and nothing explained the
other.
Removal now moves the directory aside and empties that, which makes it one operation
instead of several hundred. If the move succeeds, whatever refuses to delete next is
unreachable and gets swept at the next start while nothing has it mapped; if the move
fails, nothing was touched, which is the state where the restart message is the whole
truth. The sweep runs before any engine is created, the one moment in a session that
files left behind are certain to be releasable.
The line under the picker is decided in one pure function beside the button's, and it
has a case for a directory that is not a runtime: files incomplete or damaged, Download
replaces them, Remove clears them away - warned, because those two buttons contradict
each other without it. Reclaiming no longer consults the pinned archive size either,
which answers how big a download is and nothing about what is on disk. And after a
removal the row you cleared stays selected instead of hopping to the engine in use:
that hop is what looked like a disabled In use with no download button anywhere, when
the button was on the row that had just been left.
- Say a voice's name when somebody picks one
(ffa4d94) ~John KauffmanChoosing George (GB) was answered with "em george". The preview that fires when a
selection changes speaks the voice's own name, and it was handed the stored value to
do it with - which for Kokoro is an identifier, af_heart or bm_george, and a
synthesizer reads those as letters. Piper only looked fine because its pack names
voices in voices.json, so the stored value happens to be a name already.
An engine now answers twice. GetVoiceDisplayName is for the picker and carries the
accent somebody is choosing between; GetVoiceSpokenName is for the one place that
says a name aloud and carries neither the identifier's leading letters nor the locale
tag. Kokoro parses its ids into name and locale once, behind both, so the two cannot
drift apart, and anything not shaped like one of them - a voice kept from another
engine, an embedding named by hand - comes back untouched either way, since inventing
a name for something unrecognised is as wrong in a preview as in a label. What is
stored, matched against a config and spoken on air does not change.
- Hand piper the slot it loaded, not the name of the voice
(e14d4d0) ~John KauffmanChanging to a voice nothing was speaking yet previewed as silence, while that same
voice a moment later - on a volume change, or picked again - spoke fine. A debug
log says it plainly: "Piper previews 'Cori' through the Default player" is the one
that played, and the attempt that failed logged "produced no speech for voice
'Cori'" - which is SynthesizeNative's id argument. The preview asked piper to speak
through 'Cori', a voice name, when the model it had just loaded lives under the
testSpeaker slot. An unknown id answers with -1, no audio and no exception, which
from the outside is indistinguishable from a dead sound card.
It is a regression from commit # [2b678ce](https://github.com/kauffman12/EQLogParser/commit/2b678cea): that commit added _preparedVoice so a voice it
had just built was not built again, and in the same line started handing the
recorded name back as the speaker id. Before it the synthesis went to AdHocVoiceId
like every other call. What is loaded stays recorded the same way; only the answer
changes, and both routes through this function now log which one spoke.
Reading that -1 as commit # [4294967](https://github.com/kauffman12/EQLogParser/commit/4294967295) was the other half of it. synthesize is declared to
return long while piperApi returns int, so a failure arrived as a positive number
and only the null buffer kept this from allocating eight gigabytes for audio. The
signature says int now, which makes the size <= 0 guard mean what it says.
- Make a silent piper preview say where it died
(0b9dfd6) ~John KauffmanChanging a Piper voice can still come back with no sound, so the failure is not
the native table race that was fixed earlier. It cannot be narrowed further from
here because every step between "voice selected" and "nothing heard" fails
quietly, and does so at Debug - which settings.txt switches off by default, so a
user's log says nothing about a voice that produced nothing.
Three anomalies now log on their own: piper's loadVoice answering -1 (it offers no
reason and throws no exception), synthesize answering with zero samples, and the
preview paths in AudioManager playing nothing because synthesis returned nothing,
naming the engine and voice that came back empty. Which slot spoke a preview -
the prepared one or a borrowed trigger player - stays at Debug, since that is
normal traffic rather than a fault.
- Give piper a dozen voices to pick from
(859c692) ~John KauffmanThe pack carried six, and two of those were dataset labels rather than names. It
now carries twelve English models named in voices.json by hand - Alan, Alba, Amy,
Bryce, Cori, Joe, Kristin, Lessac, Northern English and Ryan beside the two HFC
voices - each with the locale that gets printed beside it, so Piper's list reads
the way Kokoro's does now: "Ryan (US)", "Alba (GB)".
Rebuilt the way kokoro-1.0 was: unpack the published asset, replace only voices/,
regenerate manifest.json, rezip without laying a hand on a binary. All 361 files
that came back hashed identical to what went in, and every entry the new
voices.json names resolves and agrees with its model's sample rate, so nothing
carrying an Authenticode signature was rewritten.
Six voices to twelve is a 348 MB download become a 682 MB one, which is the price
of one archive holding every voice; the pinned digest, the size the button quotes,
the table in the docs and the free space checks all move with it. Hand written
names also mean -GenerateVoicesJson has to stay out of a piper pack from here on:
it takes names from each model's _meta.name, these models carry none, and it would
quietly trade "Alan" for "alan".
- Give Kokoro the British voices too
(21b3a56) ~John KauffmanKokoroSharp has always shipped them; KokoroVoicePrefixes simply said af;am, which
left bf_* and bm_* out of the build output and therefore - through -Sync - out of
the published pack as well. Eight embeddings are 4 MB on a 228 MB download and
MisakiSharp's grapheme-to-phoneme data covers both accents, so nothing else had
to change for them to speak.
The pack changes with them but no binary inside it does. A signature covers the
bytes of one PE file rather than the archive holding it, so unpacking the
published zip, adding the voices, regenerating manifest.json and rezipping leaves
MisakiSharp, NumSharp and onnxruntime exactly as they were signed - verified by
hashing every file that came out against the file that went in. What does move is
the digest this table pins for the archive, along with its size. Swapping the
asset under its own tag is open only because nothing shipped pins the old value:
master carries no pack code at all, so no installed app is left comparing a
download against bytes that are gone. The new zip and its sidecar have to go up
with this commit, not before it.
- Name the voices the way people do
(b0cc5dc) ~John KauffmanThe voice pickers list what the engines call their voices, which for Kokoro is a
filing convention: af_nicole, bf_emma, am_fenrir. Choosing between them meant
reading a two letter grammar of accent and gender that nobody learns. They read
by name now, with the accent beside it - "Nicole (US)", "Emma (GB)" - while the
item behind the label stays the engine's id, so what gets saved to a character,
matched after an engine switch and handed to SetVoice is byte for byte what it
always was.
Each engine answers for its own names because each knows something different:
Kokoro's locale is in the voice id, Piper reads it from the model's metadata -
and voices.json may now declare it outright - and Windows already hands out
"Microsoft David Desktop", where even the "(Legacy) " marker is worth keeping in
sight. Anything an engine does not recognize comes back untouched rather than
dressed up: a picker row that stops matching its config is worse than a plain one.
- Stop a piper voice swap from eating the preview
(83f8f89) ~John KauffmanChoosing a voice does two things: it speaks a preview straight away, and it
schedules the config reload that rebinds the character to that voice 750ms
later. That rebind removes the player's native voice before loading the new
one, a few hundred milliseconds inside piperApi.dll, and the preview resolves
and synthesizes through the very same table while it is being rewired. Piper
answers a synthesis against a voice that has been removed and not yet rebuilt
with no samples and no exception, so nothing played. Changing the rate worked
because by then the swap had finished; Kokoro and Windows only note a name when
bound, so they have nothing to pull out from under a call.
A lock inside the engine now covers every change to the native voice table and
every synthesis that reads it. It sits innermost on purpose - binding takes the
engine lock and speaking takes the synthesis gate, which is exactly why they
could overlap - and nothing under it reaches for either. A voice that answers
with no audio also gets logged now, since from outside that looks like a dead
output device.
- Give the Windows engine its Use button back
(291545a) ~John KauffmanThe single action button appeared only when an engine had a runtime pack to
download, which is the opposite of what the Windows voices look like: nothing
to fetch, ready to speak with. Once another engine was active, selecting
Windows left the dialog with Close as the only control and no way back to the
built-in voices short of editing the config by hand.
Visibility follows "there is something to do here" now rather than "there is a
pack", and the three answers - whether to show it, what it says, whether it
works - are settled together in one method kept away from the controls so they
cannot drift apart again. TtsEngineActionButtonTest covers the switch case,
the Wine case where no button is the right answer, and the busy case where the
button stays put and refuses presses.
- Keep the remove files prompt to a single question
(cd26642) ~John KauffmanThe confirmation stacked the byte count and a promise that the engine can be
downloaded again onto the question. MessageWindow grows to fit its text rather
than wrapping it, so three sentences turned a short question into a banner
across the dialog. Everything else in this app that destroys something asks in
one line, so this does too now.
- Keep timer names still when the variables move
(4c1ed8e) ~John KauffmanTrigger variables are re-resolved on every full overlay render, so a timer
named after one follows whatever another trigger writes into it for the rest
of the run. That is usually what you want but not when the variable tracks
who last touched something: a buff bar naming its caster should stay with the
caster who applied that instance.
Adds **Static Timer Name** to Basic Timer Options, off by default so existing
configs keep the live behaviour. The frozen name was already there, the value
the processor resolved when it started the timer, so the overlay only had to
stop preferring the template for these timers. Static covers a single run: a
restart or the next loop of a Looping timer starts a new TimerData and picks
up the current value again. Built-in codes are unaffected because they come
from fields captured at that same moment. As a side effect a pinned name can
no longer strand a raw {variable} on the bar when the variable is cleared or
its time to live runs out.
- Fix engine switching, warm-up and pack install defects
(a3771d0) ~John KauffmanAn audit of the ITtsEngine refactor turned up races around the engine swap, a
warm-up that almost never ran, and download paths that could be neither stopped
nor rolled back. All of it is fixed here; nothing about the design changed.
AudioManager grew _engineLock, taken by every quick call into the engine (bind,
voice lookup, voice list, removal) as well as by a switch. Before that a callout
could be reading an engine while SwitchEngineAsync disposed it, which for Piper
means touching a process-wide native table that has been released - an access
violation rather than a caught exception. Warm-up was rebuilt as one bounded
worker queue that marks a voice prepared only on success and runs outside the
synthesis gate: called from inside the switch, it could not take the gate the
switch itself was holding, so every post-switch warm-up gave up and the first
callout paid for everything anyway. Cache hits now answer without the gate at
all, which is what the field comment always claimed, a zero sample rate is
rejected where the WaveFormat would have been built from it, SpeakFileAsync is
no longer async void, and engine names arriving from settings are normalized at
the boundary so a hand-edited "piper" means Piper everywhere downstream.
The engines each had their own hole. Piper kept the old voice bound to a player
when the native load failed after the previous one was removed, leaving a player
that reported a voice it could not speak: silent callouts, and a name the preview
path would happily borrow. Kokoro loaded its process-wide embeddings unguarded
and could be constructed with no voices at all, so it now guards and caches that
load and refuses to be created rather than fall through to another engine that
can talk. Windows re-proved every voice and re-enumerated SAPI on each instance;
both are cached across instances now.
Installing a pack can be cancelled, and can fail safely. The token reaches the
transfer, the archive digest, each extracted entry and each verified file -
hashing is chunked instead of one ComputeHash call, and entries are copied by
hand because ZipArchiveEntry.ExtractToFile neither reports bytes nor aborts
before an entry ends, and one entry is the 156MB model. A failed promotion puts
the retired copy back, so a pack that spoke this morning still starts; free space
is checked for the archive before the download and for the extracted tree, read
from the zip's central directory, before anything lands; the progress bar is
split across digest, unpack and verify instead of sitting at 90% for a minute.
The TTS dialog gained a Cancel button, resets its state in a finally so no
failure can leave it wedged, asks before deleting a couple hundred megabytes, and
tells "engine in use" apart from "native libraries still mapped, restart".
Also in here: the Kokoro voice folder is staged only in the app's own output
instead of every project that consumes KokoroSharp transitively; duplicated
helpers (IsInstalled, ReplaceDirectory, ComputeSha256, the dispose pair) are
deduplicated and dead surface removed; docs describe what the code now does; and
EQLogParser.Wpf.Test gains offline coverage for name normalization, the digest
helper and the pinned pack digests against docs/TtsPacks.md.
Built clean (0 warnings) and 849 unit tests pass. The new audio tests compile but
need Windows to run, as does the manual pass over download, cancel, rollback and
engine switching that this still deserves.
- Warm the voice before anything has to wait for it
(2b678ce) ~John KauffmanSpeaking with a voice costs time in places that have nothing to do with the
text: a Piper voice is an ONNX session over the model plus espeak-ng data, and
any engine's first synthesis warms the runtime behind it. That bill landed on
whichever trigger fired first after a change.
AudioManager.WarmUpVoice now pays it in the background when a player is
registered, when its voice changes - which is what the dropdown does, both for
a pick and for the default an engine switch selects - and after a switch, once
per distinct bound voice. Engines do what warming means for them: Piper builds
the native voice, Kokoro and Windows speak one short word into memory and drop
it, since their voices are already in the session made at creation.
Warm-up enters the synthesis gate only when nothing is holding it, retries
briefly and gives up rather than standing in line ahead of real speech:
choosing a voice also speaks an audible preview and that must not queue behind
a warm-up.
Piper also stops rebuilding and destroying a voice for every preview, which
meant paying for the model again on each different text. One prepared voice is
kept under the reserved id and replaced when the selection moves, a preview of
a voice some player already speaks uses that player's session instead of a
second copy, and rebinding a player removes its old native voice first because
replacing a table entry is not documented as releasing it.
- Let the picker be browsed, make Use apply
(1cc9068) ~John KauffmanSelecting an engine applied it on the spot, and Remove Files refuses whichever
engine is speaking because its native libraries stay mapped until the app
closes. Every row on screen was therefore the active one, so the button could
never become available for anything you had just downloaded: no sequence of
clicks reached it.
Selection now only shows, and a single action button says what pressing it
would do - Download for a pack with nothing on disk, Use to start an installed
engine, In use when it already speaks. Downloads still apply themselves, since
that is why they were fetched, and the saved setting is written where using an
engine was asked for rather than merely highlighted, still unwound when a
switch fails. Removing a pack now also drops the saved setting off that engine
so the next start does not name a directory that is gone.
(e6cb8a6) ~John KauffmanUpdated TTS descriptions
(18cce09) ~John KauffmanAdded comment/example
- Grey out unusable engines without a container style
(eae4f60) ~John Kauffman
- Put the vendored ONNX runtime in step with the published pack
(292cb5d) ~John KauffmanPiper-1.0 was republished with onnxruntime 1.22 standing in for the 1.14 Piper shipped, but the two binaries here never moved, so a pack built from this clone would put the old pair back into circulation. Both now hash exactly to what GitHub serves: commit # [579b636](https://github.com/kauffman12/EQLogParser/commit/579b6364)... and commit # [ba00ea1](https://github.com/kauffman12/EQLogParser/commit/ba00ea1e)..., which is also what the NuGet package for the managed wrapper drops into the build output.
This is agreement rather than an upgrade: whenever Kokoro is present Piper already runs on 1.22, because Windows keeps one onnxruntime.dll per process and the pack manager hands that name to the build matching the managed wrapper.
- Delete what installs before packs left behind
(ea7a46c) ~John KauffmanA bundled piper-tts, the Kokoro voices, and the support assemblies KokoroSharp pulled in come to about 150MB under the program folder of older installs. Nothing reads those paths any more, so this is reclamation rather than a behaviour change.
The one thing left alone is {app}\runtimes\win-x64: it also holds natives other packages need, this installer does not create the folder, and from here there is no way to say what an older install left inside it.
- Read speech engines from their pack only
(48a2623) ~John KauffmanResolveRoot had a second answer for Piper: a complete copy beside the executable, so installs predating the packs kept speaking without a re-download. A directory the app silently adopts cannot be updated, cannot be removed from the dialog, and cannot be matched against a pinned digest, and it made installed-ness depend on debris: espeak-ng data and voice models still sit in old build outputs long after leaving the repository, so a development run reported a working Piper nobody had downloaded.
One location per engine now, %LOCALAPPDATA%\EQLogParser\<engine>, owned end to end by the pack manager. What is left beside the executable is inert, and the installer deletes it.
- Keep the engine dialog inside its theme
(bb90543) ~John KauffmanText here inherited its colour from the window instead of asking for ContentForeground like every other window does, and no status line distinguished success from failure. Everything resolves through the theme dictionaries now: body text as the skin defines it, EQGoodForegroundBrush when something worked, EQStopForegroundBrush when it did not, EQWarnForegroundBrush when an engine failed to come up or has no voices to give. Switching to MaterialLight repaints instead of leaving colours chosen for a dark skin, and the ComboBox row style resolves its base through DynamicResource so it follows a theme change too.
The lines got shorter in the process, since the dialog is 420 wide and SizeToContent grows with every wrapped row. The status line is short and holds numbers: "128 MB of 224 MB", then "validating files...". It does not name the engine, because whoever pressed Download knows what they pressed.
- Make the engine dialog show what is really happening
(9613da4) ~John KauffmanTwo things were wrong when it opens. The picker rested on the saved setting instead of the engine actually speaking, so
any fallback -- pack missing, model refused, Wine with no voices -- left it naming something EQLogParser was not using,
and the line beneath it cheerfully promised that choice would take effect on the next callout. It opens on the live
engine now, and where setting and reality disagree it says so: "Kokoro is what EQLogParser speaks with. The saved
choice, Piper, would not start here." A switch that fails unwinds the setting as well, rather than leaving a preference
for an engine that just failed to initialize.
The status text shared a grid row with the progress bar, so white letters went unread under a fill growing through them.
The bar keeps its own row and the words go below it, with the byte count they were missing: "Kokoro: about 128 MB of
224 MB downloaded", turning into "checking the files and installing them..." for the last tenth of the run, which is
hashing and unpacking rather than the network. Selecting a row after a download now takes the object from the list
instead of building an equal one, so the drop down highlights it rather than holding a value its items do not contain.
- Point the piper pin at the pack on GitHub
(94a3a42) ~John KauffmanPiper-1.0 was rebuilt with onnxruntime 1.22 standing in for Piper's vendored 1.14, which moved the asset digest to
commit # [dc24d7f](https://github.com/kauffman12/EQLogParser/commit/dc24d7f9)... while TtsPackManager went on asking for commit # [059241c](https://github.com/kauffman12/EQLogParser/commit/059241c0).... Every download was then refused -- correctly, because a
pin that no longer matches really does mean "not the bytes we vouched for" -- but unhelpfully: one line, no numbers, and
nothing saying the pin was the thing to look at.
So quote expected and got, and put in TtsPacks.md where a pin comes from (GitHub's own asset digest), that the sidecar
next to an asset is for humans and -Verify rather than for the app, and that replacing an asset under its own tag is
only legitimate before some build pins it. Checked the published pack by range-reading it rather than pulling 348MB:
onnxruntime.dll at 12,418,080 bytes stamping 1.22.0, provider stub alongside, both matching manifest.json, six voices.
- Settle which onnxruntime the process gets
(18dd65b) ~John KauffmanPiper vendors onnxruntime 1.14 and Kokoro's wrapper is 1.22, and Windows keys modules by base name: whoever loads
first serves both for the life of the process. Speak from Piper, then enable Kokoro, and Kokoro's graphs are refused
with "Unsupported model IR version: 9" over a download that is fine -- the 1.22 wrapper was talking to the 1.14
runtime. Restart with Kokoro first and everything works, because the newer runtime serves Piper's older native code
without complaint. That made which engine you used first decide whether the other one loaded.
Piper now hands the name to the build published with our managed wrapper before piperApi.dll can claim its neighbour,
so the pairing is chosen rather than raced. Both engines name the module that answered in their init failures: a model
refused for its IR version reads as "the download must be corrupt" otherwise, which is a long way from the truth.
- Say Wine out loud in the engine warning
(59847b4) ~John Kauffman"Not usable, add Windows voice packs" was bad advice for the case we can
now name. The line under the picker says the voices come from Windows
itself and that neither Wine nor a Windows image without the speech
runtime has any, which leaves "enable Piper or Kokoro" as the way out
instead of an errand that cannot succeed.
- Recognise Wine before the voices fail
(af631ba) ~John KauffmanAsk ntdll for wine_get_version. Wine has exported it for over a decade,
real Windows never had it, and unlike build-number or registry heuristics
there is no service pack that can move it. Result cached; the load is
pinned to System32 so a planted ntdll.dll cannot decide the answer.
The reason to trust this one rather than only the runtime probe is that
the errors are not symmetric. A false "this is Wine" turns off the only
engine a machine has, and getting there needs Windows to grow an export
it does not have. A false "not Wine" costs what it costs today: the probe
finds nothing usable and says so. Whisky, Bottles and CrossOver are Wine
underneath; a real Windows VM on Linux keeps its voices, correctly.
- Grey out an engine this machine cannot use
(62d21df) ~John KauffmanThe picker's rows now carry whether they can be picked. An engine that is
neither usable nor downloadable goes grey where the choice is made,
rather than being selectable and then quietly refusing; in practice that
is the Windows voices on Wine, where there is nothing installed and no
download to offer either. Piper and Kokoro stay clickable while not
installed, because clicking them is how they get their runtime.
The unavailable line under the picker distinguishes those two cases --
"not installed, about 224 MB" versus "not usable on this machine, add
Windows voice packs" -- and the Windows description now says its voices
come from the operating system, so a Linux or Wine session usually has
none.
The ItemContainerStyle is BasedOn the themed style: a bare Style wins
over Syncfusion's and hands back a WPF default item, which disappears
into the dark theme.
- Make the Windows voices earn being available
(180e0fa) ~John KauffmanWindows is the one engine with nothing on disk to check, so the code
answered "available" without asking and swallowed the exception. On a
machine where the voices are not there -- Wine, a Linux emulator, a
stripped Windows image -- that leaves the app silent with no log line at
all, which is exactly what made the recent Linux experiment hard to read.
LoadVoicesAsync now records whether it could make anything speak, and
engine choice, the picker and switching all read that verdict. Unknown
still counts as available: only the engine that starts gets probed, so
hiding an unprobed one would silence people who are fine. Switching to
an engine that comes back with no voices is refused outright, which turns
"a successful switch into silence" into "Piper keeps speaking".
- Say what each speech engine costs
(9b5fa53) ~John KauffmanThree names in a dropdown was the whole decision, and it is a decision
about voice quality and about whether a ten year old machine keeps up
with callouts mid-fight. Now the line under the picker says which engine
is the fast one, which is the good sounding one, and what picking Kokoro
asks of the machine -- before the 224MB download, not after it turns out
the fight is being narrated half a second late.
It follows the selection rather than the applied engine, so arrowing
through the list reads like a menu. The state line above it stays about
availability (in use, takes effect now, or how much a download costs),
which is a separate question from what an engine sounds like.
- Retire the bundled Piper escape hatch
(980f6e5) ~John KauffmanIncludePiperTTS existed to keep speech working in a release cut before
the downloader did, and it produced a second installer name to keep
track of. Both are gone: the app can now only be packaged one way, and
the define would build a broken Piper anyway because the voice models
and espeak-ng data are no longer in the build output.
The installer keeps exactly the two assemblies EQLogParser.Audio.dll is
compiled against, KokoroSharp and Microsoft.ML.OnnxRuntime, so the seam
types resolve with no pack present and an engine reports itself
unavailable instead of failing at a type load.
- Drop the Piper data now that it ships as a pack
(08d0fa4) ~John Kauffman80MB of espeak-ng tables and voice models, deleted. They are data, not
build inputs: nothing compiles against them, and keeping them in git
meant every clone carried them whether or not anyone would ever speak
Piper. The EQLogParser-TTS data repo holds them now and users get them
through the runtime pack.
Five native SDK binaries stay (10MB), because two things need them at
build time and neither is the installer: sign.cmd signs
piper-tts\*.dll before a pack is zipped, and Build-TtsPack.ps1 -Sync
reads them out of the build output. piper-tts\README.md says exactly
that, so the next person does not re-add the voices out of caution.
Installs made by the old pipertts installer keep speaking: their copy
under {app}\piper-tts is complete and still honored as a fallback.
- Install the speech engines from downloadable packs
(c60a28b) ~John KauffmanBoth optional engines now fetch their runtime on demand instead of
carrying it in the installer, which is what holds the package near 19MB
rather than 45MB. TtsPackManager pins one GitHub release asset per
engine, verifies the archive against its pinned digest, then checks
every file against the pack's own manifest before promoting the
directory into %LOCALAPPDATA%\EQLogParser\<engine>.
Two resolver hooks make a pack loadable: the default assembly load
context answers KokoroSharp's support assemblies from kokoro\bin, and
unmanaged imports answer onnxruntime from kokoro\native. Both decline
everything they do not have, so unrelated loads are untouched, and both
use the default context rather than a private one so types stay
identical to the assemblies that install beside the executable.
Kokoro reads its voice embeddings from the pack instead of next to the
executable, so KokoroVoiceManager.LoadVoicesFromPath gets a path; Piper
resolves its root per instance and re-runs initialize() when a different
pack directory comes along. The engine-specific Kokoro model downloader
is gone: the pack carries the graph, still verified against its own pin.
The TTS Engine dialog now lists all three engines and puts the download
where the choice is, including removing an engine's files again, because
224MB deserves a way back.
- Fix the voices.json path check and report inputs up front
(34f93c8) ~John KauffmanVoices.json paths are relative to the voices directory -- "hfc_male/en_US-hfc_male-
medium.onnx" -- which is how PiperTtsEngine resolves them, but the packer validated
them against the pack root. Nothing could ever match, so a correct data dir failed
with "amy/en_US-amy-medium.onnx is not in the pack". Fixed, and the failure now
lists what that voice folder actually contains plus the closest name, because the
same message has to be useful when the data really is wrong.
Check every input before staging anything: a half-populated data dir used to blow up
from three functions deep, after the other engine's pack had already been written.
Inventory and the packing path now share one list of requirements per engine.
-Sync covers what first-run actually needs (both engines' binaries, espeak-ng-data,
Piper voices, Kokoro embeddings) and pulls kokoro-fp16.onnx from local app data or
-ModelSource, since that file is not in the build output. -GenerateVoicesJson writes
its result back into piper-tts\voices so the mapping that ships is the one in git.
- Drive the pack build from data dirs, not a build output
(89226b7) ~John KauffmanThe packer moves into EQLogParser-TTS and reads its inputs as data two directories
over -- piper-tts/ with voices and espeak-ng-data, kokoro/ with bin, native, voices
and an optional model -- because that data outlives any particular app build and is
what actually needs a home. Only the runtime binaries come from a build: -Sync copies
them in, hash-comparing first so unchanged files are left alone. -Inventory reports
what is present and what is missing without packing anything, which is the check worth
running before publishing.
Single pack per engine, as decided: one download per engine keeps the loader simple,
at the cost of every Piper user fetching all voices. Drops the per-voice split mode
and its extra manifest plumbing.
Adds scripts/tts-repo-template/ with a README and .gitignore for the new repo, and
spells out what git can hold: GitHub refuses pushed files over 100 MB, so the 156 MB
Kokoro model is release-asset only, and voice models below that still do not belong
in history that every clone carries forever.
- Build the downloadable speech runtime packs
(673f2f3) ~John KauffmanPiper and Kokoro now live on GitHub instead of in the installer, so something has
to assemble them reproducibly and refuse to publish a pack that is missing a file.
Build-TtsPack.ps1 stages the two layouts (kokoro: bin/native/voices[/model], piper:
runtime dlls/espeak-ng-data/voices), writes a manifest.json holding every file's
size and SHA-256, zips it, emits a .sha256 sidecar and prints the tag and pin URL.
It checks three things the failure modes actually punish: files we are expected to
have signed that are still unsigned (a publisher-less DLL is what AV punishes, and
Microsoft's own files must keep their own signature), voices.json entries whose
model or config file is not in the pack (a typo there is silence on a user's
machine), and -Verify to re-check an asset after download.
-GenerateVoicesJson builds voices.json from the voice folders, reading each model's
sample rate and name, which matters now that Piper is growing past one voice;
-SplitVoices emits per-voice zips on top of a runtime pack so enabling speech does
not mean downloading every voice.
- Keep the speech runtimes out of the package
(a2c10c7) ~John KauffmanPiper was either 65MB compressed in the installer or absent, and Kokoro added
93MB more. Both engines are optional at runtime already, so only the two small
assemblies EQLogParser.Audio.dll is compiled against stay in {app}; the heavy
set moves to a per-user pack under %LOCALAPPDATA% that a user downloads when
they enable an engine.
sign.cmd gains its own pack section: the files are signed even though they no
longer install, because an unsigned downloaded DLL is what antivirus heuristics
punish, and vendor-signed files (Microsoft's onnxruntime) are left alone rather
than re-signed over. Manifests have to be generated after signing, which the
release checklist now says out loud.
Fresh installs get no Piper or Kokoro until the pack loader lands; existing
installs keep reading their {app} copies.
- Switch the TTS engine without restarting
(b25c9fc) ~John KauffmanPicking another engine only took effect on the next start, which made trying
Kokoro a two minute affair of download, close, reopen, hope. Engines are
objects now, so SwitchEngineAsync builds the requested one, lets it find its
voices, re-binds every player and retires the old one under the synthesis gate.
A voice name from one engine means nothing to another, so the manager keeps
what each player was asked to speak with and replays it; an engine binds the
names it has and drops the rest, which sends that player to its default.
Kokoro now refuses to remember a name it does not have rather than quietly
speaking it as something else.
- Synthesize off the caller's thread and cache the PCM
(c7410b3) ~John Kauffman
- Put each speech engine behind an ITtsEngine seam
(ea299ae) ~John KauffmanAudioManager carried the engine choice as two booleans consulted at a dozen
sites, which meant every engine touched voice listing, defaults, per player
voice binding, synthesis and shutdown, and none of that was testable because
it all ran through statics into native code.
Windows, Piper and Kokoro now implement ITtsEngine and own their own per
player state; the manager holds one engine resolved by a factory that walks
the preference order. Same engines, same fallback order, same voices.
- Resolve piperApi.dll through an import resolver
(d6a7672) ~John Kauffman
- Verify the kokoro model against a pinned sha256
(6af3ec0) ~John Kauffman
- Keep Mandarin voices out of the package
(7c0ab55) ~John KauffmanBelt and braces next to KokoroVoicePrefixes: if a build ever produces the full
voice set again, voices-zh (51MB) still must not ride along.
- Download the fp16 Kokoro model instead of fp32
(a0b8e5f) ~John Kauffman156MB vs 310MB per user for a difference that is hard to hear on trigger
callouts; both graphs come from the same KokoroSharpBinaries release. The local
file is named after the graph so the two can never be mixed up (an existing
fp32 kokoro.onnx in local app data is simply ignored).
- Ship only the American English Kokoro voices
(169c6f2) ~John KauffmanKokoroSharp's buildTransitive CopyContent target copies all 157 bundled voice
embeddings (79MB) into every project that consumes the package transitively,
which put 4x79MB into the build tree and 79MB into the installer. Redefining
the target in Directory.Build.targets replaces it (NuGet's g.targets is
imported before Directory.Build.targets, last definition wins) with a copy of
the prefixes listed in KokoroVoicePrefixes, plus the Apache-2.0 LICENSE.
KokoroVoiceManager lists whatever .npy files exist under <app>\voices, so an
unshipped voice is simply not offered and no app code changes. The target also
prunes anything outside the configured set so a changed mask heals old outputs;
it runs only where KokoroSharp.dll was actually copied locally.
- Drop the unused clr-namespace from TtsEngineWindow
(0a68e0e) ~John Kauffman
- Ship and sign the Kokoro TTS runtime payload
(27213ac) ~John KauffmanKokoroSharp.CPU adds managed assemblies (KokoroSharp, MisakiSharp,
NumSharp, Microsoft.ML.OnnxRuntime, System.Numerics.Tensors, the OpenTK
bindings it references) plus a native onnxruntime under
runtimes\win-x64\native that the host resolves from deps.json, and drops
the ~79MB voice pack in <app>\voices where KokoroVoiceManager looks for it.
The .iss and sign.cmd lists are curated minimum sets (docs/ReleaseChecklist.md),
so every one of those has to be listed explicitly.
Still to do on Windows: run scripts/MeasureLoadedAssemblies.ps1 with Kokoro
selected and reconcile the report against both lists.
- Keep Windows voices listed when an engine is preferred
(bc84311) ~John KauffmanLoadValidVoicesAsync() short-circuited on PiperTts.Initialize(), so with
TtsEngine=Windows and a piper voice pack installed the Windows voice list
came back empty even though the Windows engine was running. Use the flags
resolved in the constructor instead; that also avoids Piper's Initialize()
side effects (SetDllDirectory + espeak init) on non-piper sessions.
(20bacf1) ~John KauffmanMerge PR #406: selectable TTS engine (Windows/Piper/Kokoro)
Pulls in willk's KokoroSharp-based Kokoro engine and the TTS Engine picker.
EQLogParser.Audio.csproj conflicted with develop's package bumps; kept the
newer log4net/Caching.Memory versions and added KokoroSharp.CPU.
- Pin the Contents heading and scroll only the link list
(58f387a) ~John Kauffman.toc carried overflow-y: auto, but the <h1>Contents</h1> lives inside .toc, so scrolling
the long two-level list scrolled the heading out of the sidebar as well. Make .toc a flex
column at desktop widths with the overflow on the inner <ul>: the heading stays put, the
list scrolls under it, and min-height: 0 lets the flex child shrink instead of growing the
sidebar past the viewport. scrollbar-width: thin plus a little padding keeps the scrollbar
off the longest link. CSS cache version -> 17.
- Keep FAQ questions out of the TOC, scoped to that page
(a98c21b) ~John KauffmanThe FAQ questions are full sentences up to 100 characters, which wrapped to three or five
lines each in the 200px sidebar: the contents list measured 1322px tall against a 724px
viewport, so Feedback and everything below 'right-click Copy' was unreachable without
scrolling the sidebar twice. TOC_HIDE_CHILDREN drops the nested links under the F.A.Q
heading while leaving the heading itself listed; Linux Support and Feedback keep their
shorter sub-entries, and getting-started/documentation/policy are untouched.
- Real 404 page for missing URLs
(74bd5e1) ~John KauffmanMissing URLs currently return Amazon's raw NoSuchKey XML body with
Content-Type: application/xml, which is useless to a visitor and wastes the one chance
to redirect someone who followed an out-of-date link. Adds dist/404.html built from the
same head/nav/theme pipeline as every other page: download button plus links into the
docs and the release notes feed.
No AdSense unit on this page, because Google does not allow ads on error pages and a 404
that renders the ad rail is an invalid-traffic risk. Analytics stays, with a
file_not_found event carrying the path and referring URL, so rotted links become visible
in reports instead of silently disappearing. Marked noindex with no canonical, kept out of
the sitemap, and added to the upload script. build_head gained an indexable flag for this.
- Site checks that fail the build on broken or shift-prone output
(616a732) ~John KauffmanAdds website/sitecheck.py, run at the end of every build (also standalone:
"python sitecheck.py") and exit-non-zero worthy. It verifies each page has a nav bar,
a title and a description, that every local href/src resolves inside dist/ and every
in-page anchor exists, that images all carry width+height so nothing shifts while they
load, that ad slots stay fixed size with full-width resizing off, that the sitemap parses
and covers every indexable page, that feed.xml entries point at real anchors, and that
ads.txt still names the publisher used by the page slots.
Proven non-vacuous by deleting things from the built output: it reports missing nav,
dead anchors, dead page links and unreserved image boxes (exit 1). Writing it caught a
real mistake of mine in the process: ads.txt correctly lists "pub-…" rather than the
"ca-pub-…" the snippets use, so the publisher id is normalised before comparing.
- Atom feed for release notes (dist/feed.xml)
(2b5c990) ~John KauffmanA desktop tool lives or dies on people hearing about new versions, and right now the
only way is watching the GitHub repo. Generates the latest 20 releases as an Atom feed,
advertised by rel="alternate" in every page head plus a visible "Subscribe (Atom / RSS)"
entry in the release notes TOC.
The feed is built from dist/releasenotes.html rather than the markdown so entry permalinks
reuse the anchors the page actually has (#2-3-61-08-31-26) instead of re-deriving slugs a
second time and drifting. Verified: 20 entries, XML parses, every entry anchor exists in
the built page, and the year-marker spans inserted for the by-year nav stay out of bodies.
Also regenerable on its own with "python build.py feed". status.html is the one page that
does not advertise the feed, which is fine since robots.txt disallows it anyway.
- Pin the AdSense unit to its declared size
(2586259) ~John KauffmanData-full-width-responsive="false" stops AdSense replacing the 160x600 skyscraper with a
wider unit that no longer fits the column it was placed in, which is how ad code normally
buys layout shift. The attribute is set explicitly rather than relying on the inline width
so a later edit cannot reintroduce it; index/status keep their own copy of the snippet in
their templates and were updated to match.
- Upload images, vendored scripts and favicon with real cache lifetimes
(02ff169) ~John KauffmanNothing under img/ or assets/ carried a Cache-Control header at all (CloudFront fell back
to its 24h default) and the stylesheet was sent as no-store, so every page view refetched
the 15 KB CSS. Assets now get 30 days; the stylesheet gets a year immutable because it is
addressed as style.css?v=<CSS_VERSION> and the distribution's CacheWithVQuery policy keeps
'v' in the cache key, so bumping CSS_VERSION is a complete invalidation. Images are not
content-hashed, hence 30 days rather than immutable, with the invalidation command noted
in the script for the day a screenshot is swapped in place.
- Complete the font stacks so macOS/Linux stop falling back to a generic face
(6e11fc3) ~John KauffmanThe sheet asked for 'Inter', which nothing ever loads, so on anything without a local
Inter the only match was the generic sans-serif. Windows is unaffected (Segoe UI was
already the effective choice); this makes the other platforms use their UI font and
gives monospace a real fallback chain instead of Consolas/Monaco, neither of which
exist on Linux. CSS cache version 16.
- Nest H2 sections under their H1 in the docs TOC
(6522454) ~John KauffmanThe TOC listed only H1s, so getting-started.html offered a single link for ten
sections and documentation.html two for twenty-three. Sub-entries now use the
.toc a.sub styling that already existed in style.css with no markup using it, so
this is presentation-only. Verified: 11/25/21 links generated and every anchor
resolves, layout columns unchanged at 1366px (200px 914px 160px), TOC still hidden
by default and click-to-open at <=900px.
Heading ids are now deduplicated (a numeric suffix on repeats) because two headings
with the same text produced identical ids and every link jumped to the first one.
At >=901px the TOC scrolls inside its own column so a long list cannot hide its
tail; narrow screens keep page-level scrolling. CSS cache version 15.
- SoftwareApplication JSON-LD on the home and download pages
(64eff4b) ~John KauffmanGives Google the structured facts it can show as a rich result: name, version,
operating system, free offer and the installer URL. Both are taken from the values
the build already derives (Inno Setup version, release asset URL), so the markup
stays correct on every release without anyone remembering to edit it. No aggregateRating,
since there are no ratings to describe and fabricated review markup is a penalty.
- Per-page <title> and meta description from one PAGE_META table
(2c882c2) ~John KauffmanEvery docs page shared one generic 240-character description and the home page
<title> was just "EQLogParser", so search results had nothing to match queries
against. Copy now lives in PAGE_META keyed by output file, is formatted with the
current version where relevant, and og:title reuses it (previously it prefixed the
brand a second time). index.html's title/description are applied at build time so
the template cannot drift from the table.
- Fix desktop CLS and narrow-screen TOC, add sitemap + SEO meta
(546d141) ~John KauffmanGrouped because these all touch build.py's shared head/template pipeline.
- CLS: stamp real width/height on every built <img> so browsers reserve the box
before images load (the missing dimensions were the desktop CLS > 0.1 cause);
img { max-width: 100%; height: auto } keeps aspect ratio; hero logo is now
eager + fetchpriority=high because it is the LCP element.
- TOC: CSS owns the collapsed default (html:not(.toc-open)) at <=900px, so the
first paint never shifts; the toggle flips toc-open on <html>, persists it and
is restored pre-paint by the head script. Removes the dead .layout.toc-collapsed
rules that put <main> into an implicit 200px grid row at narrow widths.
- sitemap.xml generation (7 URLs, git-based <lastmod>) + Sitemap: in robots.txt,
XML/txt upload labels in update-aws.cmd, status.html excluded (robots disallows).
- SEO/social meta: canonical per page, theme-color light+dark, og:url/site_name/
image size, twitter:image, sitemap link; descriptive alt text and a build warning
for any <img> without alt.
- GA4: wire the real measurement id (was the G-XXXXXXXXXX placeholder).
(4f6d087) ~Will KindermanAdd selectable TTS engine (Windows/Piper/Kokoro) with Kokoro neural voices
Bug Fixes
(97fc2b0) ~John KauffmanFix for reset trigger color
(75343da) ~John KauffmanFix duplicate tree on trigger import
- Replay early quick-shares, batch import state writes, retune Core analyzers
(a24cf14) ~John KauffmanQuick Share lost anything accepted before the window existed. GinaUtil now feeds
QuickShareState (Core cannot see the WPF manager) while the manager mirrors records into the
bound collection from its constructor, so a GINA share seen before the window was ever opened never
appeared in that list, even though OpenQuickShareStatusAsync, which reads the state directly,
still showed it. QuickShareState.Subscribe attaches and replays under one lock, so a view created late
cannot miss a record; replay runs oldest-first to match the manager's Insert(0). The legacy share
flow kept working because it touches QuickShareManager.Instance itself — that asymmetry is what
hid the bug. Two tests pin the replay order and the empty-history case.
Import wrote each character state document once per merged node, re-serializing the whole Enabled
dictionary every time: quadratic in import size. 600 triggers for 8 characters measured 752ms and
measures 27ms after collecting the touched documents and writing them once at the end of the import
(300 for 8: 169ms -> 20ms). The flush stays inside the caller's transaction, so nodes and state
still commit as one unit.
Also fixed in the files above:
- GetAllOverlays handed back a deferred LiteDB cursor that its callers enumerated after the queue
callback returned, i.e. off-queue against a handle another transaction may be holding.
- A node without an id — which UpgradeTree elsewhere explicitly tolerates — took down the whole
view build, because the badge dictionaries and the enabled-state lookup throw on a null key.
SetStateFromParentInternal likewise walked every root folder when asked about no node at all.
- GinaUtil.CheckGina dereferenced the trusted-player list, which TriggerProcessor may legitimately
set to null from a TrustedPlayers update; it now matches TriggerUtil's tolerant check.
- A dead store in the heal-window loop, and ValidateOverlays resolving the overlay id set once per
element instead of once per import.
EQLogParser.Core gains the AnalysisLevel/EnforceCodeStyleInBuild settings the app project has; the
assembly split had silently dropped them for the ~40 moved files. The fallout is fixed rather than
ignored (culture-sensitive EndsWith/ToLower, IndexOf-for-Contains, default-valued initializers,
culture-bound AppendLine in the NAG report), and the three process-lifetime singletons carry a
documented CA1001 suppression pointing at the teardown that actually owns them.
Removed code nothing could reach: AdpsTracker.UpdateLandsOnYou/SetCritRateMods (Recalculate owns
those values), PlayerRegistry.GetPetPlayerMappings, TriggerLogManager.ClearCharacter, two
ViewOptionRegistry accessors plus the write-only field behind them, and 62 commented-out lines in
HitFreqChart that referenced a type no longer in the repository.
Reduced duplicated code to one copy: QuickShareRecord.FromChat (both share producers carried the
same ownership and "To" rules verbatim), GinaUtil.TryGetGinaKey (its marker arithmetic existed
twice), and TestWorkspace — five fixtures each kept their own temp-dir list, [TestCleanup] delete
loop and CreateDirectory(Guid) line, two of them also their own FindRepoFile.
Left alone for a decision: EventsNodeCheckChanged and its tree walk (redundant on every current
path, but asserted by a test), the full GINA/legacy pipeline merge, the stats-builder and WPF
grid-editor duplication.
Build clean in Debug and Release with no code warnings; 849 tests pass (was 847).
- Keep the GINA chunk download off the dispatcher
(36505d5) ~John KauffmanRunGinaTaskAsync is async but its body is synchronous HTTP: up to 100 sequential
DownloadPackageChunk round trips through HttpClient.Send / ReadAsStream / CopyTo,
with no cancellation token and the default 100 second timeout. Only the leading
Task.Delay awaited — so where that continuation resumed decided where the whole
download ran.
From TriggerProcessor's chat task there is no context to capture and the transfer
merely occupies a thread-pool thread. But QuickShareWindow's Import button calls
ImportQuickShare from the UI thread, so await captured the WPF dispatcher and
resumed the download on it: the window stops painting and overlays stop updating
until the transfer ends. Resuming on the thread pool is safe because nothing in
the method touches WPF directly — the GinaPlatform hooks marshal themselves and
TriggerStateDB has its own queue.
The GINA service is effectively gone, so today this path fails fast and the freeze
is mostly theoretical; ConfigureAwait(false) is a one-word guarantee rather than a
dependency on how quickly eq.gimasoft.com answers. Verified the resume behaviour in
a throwaway harness with a dispatcher-like SynchronizationContext: without it the
continuation runs on the owner thread (13 == 13), with it on a pool thread.
- Bind node ids instead of interpolating them into commands
(f750b9b) ~John KauffmanSetExpanded built "… WHERE _id = '<id>'" and SetAllExpanded built the same
statement without a predicate. Execute() feeds LiteDB's command parser, so an id
carrying a quote broke the text: every expand/collapse of that node threw
LiteException ("Unexpected token `id` in position 53") and its state was never
saved, for as long as the node exists — the id is stored in the database.
Such ids are reachable because an imported overlay keeps the id from its file
(that is what makes re-export round-trips work), so a shared .ogf / Quick Share
payload chooses that string. The UI hides it: the throw escapes the async void
NodeExpanded handler into AppDispatcherUnhandledException, which logs it and sets
Handled, so the symptom is one node that will not remember being expanded plus a
growing error log.
Both values are now bound as @0/@1 parameters (LiteDB 5 rejects the "?" form).
The stored document is identical — Boolean True either way — so normal ids behave
exactly as before; Execute() runs a single statement, so nothing worse than a
failure could ever be smuggled in.
- Never lose a failed dispatcher post; read quick-share state safely
(f70ac45) ~John Kauffman- UIUtil.InvokeAsyncLogged continued only with OnlyOnFaulted, so it observed neither an
already-completed faulted task (the inline path) nor a dispatcher post cancelled by shutdown:
the callback simply never ran and nothing was logged. The continuation now fires for anything
that did not run to completion — faults at Error, cancellation at Debug because it only happens
during teardown, and on TaskScheduler.Default so logging does not depend on the dispatcher that
may itself be shutting down.
- OpenQuickShareStatusAsync enumerated the window's bound ObservableCollection from a background
thread; it now takes QuickShareState.Snapshot(), which is lock-guarded and WPF-free.
- QuickShareManager.Instance had a public-ish setter and an internal constructor even though the
constructor subscribes to QuickShareState.Accepted: constructing a second manager would mirror
every accepted record twice. Setter and constructor are now private.
- Keep folder media badge across siblings; never reuse a taken _id
(76c6d37) ~John Kauffman- Import assigned the per-node CheckMissingMedia() result instead of OR-ing it, so a clean
sibling imported after a broken one cleared the running flag and the containing folder lost its
missing-media badge. Both call sites now accumulate with |=.
- Overlay insert reused the exported node id unconditionally. _id is unique across the whole
collection, so importing the same share into a second folder threw on insert and rolled back the
entire import. The exported id is now used only while it is free (its re-export match case);
otherwise Insert() generates one. This is routine expected behaviour, so nothing is logged.
- Two store tests pin both behaviours; both fail against the pre-fix code.
(51d0b0a) ~John KauffmanFix litedb serialize
- Blank line before lists so python-markdown renders them
(87bc5a0) ~John KauffmanPython-markdown does not let a list interrupt a paragraph (unlike
CommonMark), so lists placed directly after an intro sentence were
swallowed into the <p> and rendered as inline text. Add the required
blank line in 4 places in getting-started.md and 4 in triggers.md.
Documentation
- Add DesignNotes for trigger and overlay import, referenced from CodingStandards
(6c73382) ~John KauffmanRecords the import behaviour we settled on so it is not rediscovered or quietly reversed: what each
producer (tgf.gz, ogf.gz, gtp, EQLP quick share, GINA share, NAG migration) puts on the wire and what
identity it carries; why the overlay tree is flat and must be treated as such; the overlay matching
contract (Id when present, Source only when there is none, never the name) with the reason — NAG mints
overlay ids per install, so Source is a same-lineage migration key rather than a cross-user identity;
id reuse on insert; threading rules for the share pipeline; media validation and badge accumulation;
name trimming because LiteDB trims stored strings. Plus the warts we reviewed and left alone, and the
alternatives we rejected (name matching, Source before Id, logging routine collisions).
CodingStandards now points at it from the top, and gains the rule that design rationale belongs in
DesignNotes instead of long paragraphs of code comments: the comment states the constraint and links
to the note.
- Bind values in LiteDB commands instead of building them in
(6a4ed88) ~John KauffmanAdds a short Data Access section: IDatabase.Execute parses command text, so values
belong in @0/@1 parameters (LiteDB 5 has no "?" placeholder) while collection and
column names, being constants, may still be interpolated. Imported payloads decide
some of those values — an overlay keeps the _id from the shared .ogf — which is
exactly how SetExpanded came to throw on a node id containing a quote.
- Logging is for exceptions and session milestones, not expected behaviour
(e29e740) ~John Kauffman
- Fix mislabeled store defaults and re-attach orphaned NagUtil comment
(1a2f809) ~John Kauffman
(ca7cb28) ~John KauffmanDoc updates
- Restructure Getting Started - parser first, triggers second
(5805e55) ~John Kauffman- 'What is EQLogParser?' now states the parser-first, triggers-optional split
- New '2. Open Your First Log': File -> Open and Monitor Log File flow with
the Auto Monitor Last Log tip (parser value with zero configuration)
- '3. Audio Triggers and Overlays' covers Trigger Manager basics and the
basic-mode master switch; OBS link points at the FAQ entry
- New '4. Have More Than One Character?' explains Switch to Advanced,
Manage Characters (New -> New Character + Select Log), and per-character
enable states - replaces the old Configure Your Character section which
referenced the Advanced-mode-only character pane without mentioning it
- GINA/NAG import sections renumbered 5/6; FAQ NAG link follows to
#6-migrating-from-nag
- Flatten Getting Start - drop redundant Quick Start heading
(4d0f768) ~John KauffmanThe page itself is the getting started guide, so promote the five
numbered steps from H3 to H2 and remove the 'Quick Start' wrapper.
Point the FAQ's NAG cross-reference at the new 5-migrating-from-nag
anchor (replaces the removed quick-start anchor).
- Add 'Moving to a New PC' section covering backup/restore
(f1f19a1) ~John KauffmanExplains the Tools -> Create Backup File and Tools -> Restore From
Backup workflow for migrating settings (characters, triggers,
custom variables, overlays) to a new machine.
Features
- Replace empty release notes TOC with browse-by-year nav
(9bc2ab1) ~John KauffmanThe release notes TOC was removed long ago because 60+ entries were
too many, leaving an unused 200px column. Group releases by year
instead (2026 / 2025 / older versions) so the left sidebar is useful.
Existing H1 slug ids are untouched so the version-hash anchor
compatibility script keeps working.
Refactor
- Drop the now-tautological FindIndex guard
(4f03880) ~John Kauffman
- Drop dead branches, extract shared capture-group naming
(8e07a85) ~John Kauffman- remove the comment-only 'if (hasClassLevels)' block and the unreachable
isSequential arm of the reason ternary (the skip returns before it is read)
- extract FindFirstUnnamedGroup/NameFirstUnnamedGroup: the same while-loop was
written three times (HasUnnamedCaptureGroup + both set-variable loops)
- merged end-early phrases are already unique by phrase, count them directly
- drop the redundant second _audioFileMap reset in LoadAudioFileMap
Styling
- Link the notes instead of restating them in comments
(e26ac4f) ~John KauffmanPractices the new rule on the same branch that introduced it: three comment blocks written this
session (the load-bearing ConfigureAwait in GinaUtil, parameter binding in SetExpanded, overlay id
reuse on import) became paragraphs whose content now lives in docs/DesignNotes.md and
docs/CodingStandards.md. Each is reduced to the constraint plus a pointer, so there is one place to
update when the reasoning changes. No code change; build and tests unchanged (847 passed).
- Prefer pattern matching, plain comments for Core utility members
(6a3fa58) ~John Kauffman- x != null / x == null -> is not null / is null outside LiteDB predicates
(TriggerImportPlanner, QuickShareState, TriggerTreeViewBuilder, and the
SetState/SetExpanded/CopyState guards in TriggerStateDB)
- FirstOrDefault(...) != null -> Any(...) in QuickShareState.IsMine
- /// headers on internal utility members -> // per CodingStandards (XML docs
are for WPF component class headers): CombatRecordLookup, GinaPlatform,
QuickShareState, NagUtil, TriggerStateDB.Dispose
Commits
(70ad189) ~John KauffmanV2.3.61
(6a8d0ef) ~John KauffmanUpdated spell data and some library versions
(34a5ec6) ~John KauffmanRestore the first-versioned-run bootstrap gate and document db versioning
The isNewDb (empty-file) gate silently skipped the first-run default overlays
and pre-upgrade 'last db file' backup for populated databases that were never
version-stamped (pre-2024 builds). Capture the missing-version-document signal
before ApplyDatabaseMigrations runs - matching the old fixVersions.Count()==0
semantics - so those databases get bootstrapped, while already-versioned user
databases still never are (new tests pin both directions).
also:
- type the legacy 'Version' cleanup as BsonDocument; it is count/delete-only and
legacy documents may be unreadable System.Version values
- record the versioning history (System.Version 'Version' -> string 'FixVersion'
chain) in comments at the constants and call sites so the why survives
(f446e77) ~John KauffmanRework NAG overlay identity and drop the unused trigger metadata
nag import:
- overlay node ids stay store-generated UUIDs; the NAG overlayId now travels in
OverlayData.Source ("nag:{overlayId}") instead
- re-importing an existing NAG overlay updates it in place (TriggerStateDB.Import
matches by Source among siblings) rather than adding a duplicate copy
- trigger SelectedOverlays references are remapped from NAG ids to stored uuids
(NagUtil.BuildOverlayIdRemap/RemapOverlayReferences) before the triggers import,
so an import never references dead ids
- remove NagTriggerMetadata: nothing consumed it; per-trigger report data already
lives on NagImportResult (ConvertTriggers now returns nodes + results)
standards:
- reorder NagUtil members by visibility (fields -> internal -> private)
- fix CovertToTriggerNodes typo in GinaUtil (+ call sites)
- CombatRecordLookup hook back to unannotated Func<string, string> (Core is
Nullable-disabled); the test fallback becomes "", which both consumers treat
identically to null via IsNullOrEmpty
- drop the empty ItemGroup from EQLogParser.Wpf.Test
(5b1954d) ~John KauffmanAddress code review findings for the Core split branch
store:
- create the task queue before migrations and isolate migration failures so a
throw cannot NRE the cached singleton; version stamp only after a fully
successful run so partial migrations retry
- fail fast with InvalidOperationException when TriggerStorePlatform.GetDbFile
is unwired instead of building a no-op store over a null path
- kind-safe matching on the OriginalId import path: a folder wrapper can no
longer reach the overwrite branch and erase a stored trigger's data
- validate SelectedOverlays on both insert and update-in-place; the id set is
loaded once per import instead of one collection scan + N seeks per trigger
- load siblings/subtrees once per import and state walk (GetTree,
FixEnabledState, UpdateChildState, Import) instead of one query per node
- FixColor fallback is now #FFFFFFFF (opaque white, AARRGGBB like all other
paths; bare #FFFFFF binds as transparent black in WPF)
nag import:
- tolerant JSON readers (ReadNumber/ReadInt/ReadLong/ReadBool) at every
numeric/boolean site: string, fractional and out-of-range tokens no longer
abort a trigger and report it Skipped
- NagImportStatus enum replaces the Imported/Partial/Skipped magic strings;
NagImportResult/NagTriggerMetadata back to internal
host/ui:
- GinaUtil: dispose the request/response chain (it owns the socket) and use
CopyTo for the chunk payload - a single Stream.Read could return short and
truncate the download
- non-blocking InvokeAsyncLogged posts for EventsNodeCheckChanged and quick-
share mirroring: no dispatcher wait from inside an open transaction, no
unobserved task exceptions
- trigger tree builder tolerates an orphan node with null Parent
- NodeCheckChanged renamed to EventsNodeCheckChanged per the Events prefix
standard
cleanup and tests:
- LegacyOverlay.ToOverlay uses the generated Mapperly deep clone; new
OverlayCloneTest asserts every Overlay field survives the port
- drop dead separator constants; ParseDamage classAbility string -> bool flag
- [DoNotParallelize] + save/restore for every test class that mutates
process-wide state; regression tests for kind-mismatched OriginalId import,
stringy/fractional/out-of-range JSON, overlay re-import and unwired db path
docs: README desktop runtime bumped to 8.0.30; ReleaseChecklist requires
MeasureLoadedAssemblies for both the app and BackupUtil before release
(20dc505) ~John KauffmanAdded damage shields as melee adps
(2b50a40) ~John KauffmanUpdated download link for dotnet
(f560297) ~John KauffmanMerge branch 'master' into develop
(2741f25) ~John KauffmanUpdate bottles with new label for dotnet8
(9a832d4) ~John KauffmanMerge branch 'docs/site-fixes' into develop
# Conflicts:
# .gitignore
(a3dedb2) ~John KauffmanRemove orphaned documentation.md (superseded by getting-started/triggers/faq)
(0977945) ~John KauffmanAdded release checker to verify DLLs needed for install and some nag import bug fixes
(d4aea91) ~John KauffmanCode cleanup
(9ee7f17) ~John KauffmanNAG import: plainer report wording for no-duration timers
'Indefinite timer duration' was jargon for the one line users actually
read when triaging a Partial row, and 'end-early phrases' mixed in EQLP
naming. Reworded to plain language:
No set timer duration: this NAG timer has no fixed length; it runs
until its stop phrase matches. Imported disabled (placeholder
duration) with its stop-phrase, warning and end data populated —
set the real duration on the timer and enable it.
Internal dropped-feature note renamed to 'no set timer duration (timer
left disabled)' to match; test assertion updated.
(984c66a) ~John KauffmanAdd real-export import validation + checked-in mini fixtures
Validated against the user's full exports (kept under gitignored local/):
- allraid.tgf.gz: 467 trigger leaves / 249 folders, depth-9 nesting —
Import_AllraidTgfGz_RoundTrips checks exact node count, byte-for-byte
re-import idempotency, the B4 no-same-name-leaf invariant at depth and
regex patterns with escaped dots
- cooldownOverlay.ogf.gz: real timer overlay — stored id survives insert
and re-import is duplicate-free on top of the bootstrap defaults
Both are CI-safe skips when the files are absent (same convention as
wizard.tgf.gz).
Checked-in subsets for deterministic coverage on every machine:
- EQLogParser.Test/data/mini.tgf.gz — the real '25th Anniversary' raid
section from allraid (14 nodes, timers/warning slots/Comments,
same-name leaves under different parents)
- EQLogParser.Test/data/mini.ogf.gz — the real Cooldown Overlay plus one
text overlay
Copied to bin as mini-data\ by the test csproj; MiniFixtureImportTest
hard-fails (no skip) if a checked-in fixture is missing and pins
import/re-import idempotency, id preservation and pattern fidelity.
(28f2778) ~John KauffmanPin the standard .ogf overlay import contract with a regression test
Standard (file-based) overlay re-import matches leaves by stored id —
exports carry it for overlays only, folders export with Id null — and
updates data in place without touching the display name. This path has
no fixture-driven test on CI (wizard.ogf.gz is local-only), so add a
synthetic round-trip that pins: id survives insert, re-import by id is
duplicate-free, data updates in place, name unchanged (master behavior).
Standard .tgf path stays covered by the real wizard.tgf.gz round-trip
plus the B4 collision invariants.
(44b1a6a) ~John KauffmanFix re-import corruption for NAG triggers with shared-OriginalId fan-out nodes
A single NAG trigger can export several siblings that share one OriginalId
(phrase + timer variants, counter resets). FindExisting matched by source id
alone (FirstOrDefault), so on re-import EVERY incoming family member
overwrote the first stored sibling found in collection order: the last
incoming node's data won and the other stored members stayed stale.
Matching is now family-aware in TriggerImportPlanner.FindExisting:
- more than one stored sibling carries the id -> name must disambiguate
- the incoming batch itself carries the id more than once (first import: an
earlier member was already inserted into the live sibling set) -> name
must disambiguate too
- exactly one stored sibling, id unique in the batch -> id alone, so a user
rename still updates in place instead of inserting a duplicate (B4)
A renamed member of a shared-id family now inserts as a new, visible sibling
instead of silently overwriting another member. The store computes the
batch's shared-id set per parent level and passes it to the planner.
Adds regression tests: planner-level family/batch cases plus an end-to-end
store re-import test (both members receive their own data). 817/817 green.
(b0e0e64) ~John KauffmanReview cleanup: member ordering in GinaPlatform, drop no-op self-assignment
Final branch review pass (master vs develop):
- GinaPlatform: move the internal ImportChoice enum after the public hooks
to match the CodingStandards visibility ordering (public -> internal)
- TriggerStateDB: remove pre-existing dead line
'newNode.OverlayData = newNode.OverlayData;'
(f8f9e4a) ~John KauffmanRoute the marker migration through the intended FixVersion version chain
The previous fix added a separate boolean stamp document to FixVersion,
which bypassed the collection's actual purpose: a stored version number
gating one-time upgrades (no version -> oldest upgrades; 1.0.x -> next
set; current -> nothing).
ApplyDatabaseMigrations now owns the chain: it reads the version from
the existing {Id:"1"} document older builds wrote, applies every
migration step below CurrentDbVersion (1.0.2 = strip the stale
ExportTriggerNode type marker), and upserts that same document to the
current version only after all steps ran, so an interrupted run retries
next launch. Fresh databases are stamped at CurrentDbVersion directly;
the bootstrap block no longer writes its own 1.0.1 seed (single writer).
Future migrations append as ordered steps: bump CurrentDbVersion, add
'else if (stored < ...)' - exactly the no-version/1.0/1.1 scheme this
collection was designed for. Tests now assert the version bump instead
of the removed stamp.
(6f11ad4) ~John KauffmanFix all nullable reference type warnings in tests and Core
19 CS86xx warnings were being emitted by files added during the refactor,
contrary to docs/CodingStandards.md (Nullable Reference Types section):
proper annotations, never suppression.
- NagUtil (Core, project has NRT disabled): drop the two 'List<string>?'
annotations from the phrase-routing methods - they can only carry
meaning in a nullable-enabled context and produced CS8632 here
- test helpers that legitimately take or produce null: annotate the
parameters/returns ('string? originalId = null', 'QuickShareRecord?',
'out string?' on TryLoadNagDump) instead of passing null literals to
non-nullable parameters (CS8625/CS8603/CS8604)
- read-only guards where a null must be impossible: ReadExport throws
on an un-deserializable fixture; LoadFixture guards the manifest name
before GetManifestResourceStream; Store() guards GetDirectoryName
- GinaQuickShare lookups now bind via '?? throw' so the compiler can
track non-nullness at the use sites (old Assert.IsNotNull + deref
pattern left them untracked)
Full solution build is now warning-free.
(e58445f) ~John KauffmanMake the marker-strip sweep a true one-time migration
As shipped in commit # [d6f1ca1](https://github.com/kauffman12/EQLogParser/commit/d6f1ca1a) the cleanup re-scanned every document in every
collection on every launch. It was always a no-op after the first run,
but on a database with thousands of nodes that is wasted startup work
forever.
Gate the sweep on a stamp in the existing FixVersion collection (where
the old host already keeps its one-time upgrade stamps): while the stamp
is missing, run the full sweep; once it is written, startups pay only
for a tiny FindAll on FixVersion and skip. The stamp is written only
after a complete pass, so an interrupted run retries next launch.
The bootstrap block used an empty FixVersion as its "brand-new file"
signal - now that the stamp can live there, capture true freshness (no
collections at all) before the sweep runs instead.
Regression tests: stamp present after a clean sweep; a fresh stale
marker in an untouched collection survives a second open (sweep really
did not re-run).
(d6f1ca1) ~John KauffmanMake the legacy marker cleanup actually survive a real database run
The previous pass aborted on the first write: the store opens its LiteDB
file with ConnectionType.Shared, where starting a write invalidates any
still-open query cursor ("no more active transaction for this cursor").
FindAll() is lazy, so per-document Update calls killed the cleanup loop
mid-sweep; the constructor catch swallowed it and only ~one document got
cleaned per launch - which is why old databases kept crashing.
- materialize the read before writing (FindAll().ToList())
- sweep every collection, not just Tree, with per-collection error
containment so one bad collection cannot block the rest or startup
- run the sweep first thing after opening the file, before any typed
query, so no earlier failure can skip it
- log which database file is opened and log open failures at Error level
with the exception (previously Warn, easily missed)
- null-safe Dispose for constructors that failed early
Regression test now seeds markers in Tree (top-level + nested child)
and in Config and asserts every collection comes out clean.
(25245c7) ~John KauffmanFix unreadable legacy trigger databases (stale LiteDB type marker)
Databases written by pre-refactor builds carry LiteDB's polymorphic type
marker "_type": "EQLogParser.ExportTriggerNode, EQLogParser" on imported
nodes. ExportTriggerNode now lives in EQLogParser.Core, so resolving the
marker throws 'Type ... not found in current domain' on the first tree
query — the whole database becomes unreadable (ctor, GetTree,
FixEnabledState, LoadOverlayStyles all crash).
The marker is inert: nodes were always stored flat via Parent/Id links and
the export type persisted nothing beyond TriggerNode itself. The constructor
now strips the stale marker from any tree document before the first query —
an idempotent no-op for databases the current build writes, so no data is
touched otherwise. Cleanup reports a single summary line (databases hold
thousands of nodes) and only on the first launch after an upgrade.
Regression test seeds a legacy-marker document into a real LiteDB file and
opens it: without the migration it reproduces the exact LiteException, with
it the store opens and the node survives intact.
(612c4ae) ~John KauffmanNAG import: null-duration timers import disabled instead of a guessed 60s
Neither NAG nor we can say what an indefinite timer's real duration is, so
stop inventing one. A NAG timer action with duration: null now imports with
EnableTimer = false and the model's default duration (0.2s), while every
other field still populates as before: end-early phrases, warning text/
sound/duration, end texts, label, colors, overlays. A user who knows the
real duration sets it and flips the timer on — no re-import needed.
- TimerActionData gains an explicit Enabled flag; the fan-out derives
EnableTimer from Enabled && DurationSeconds > 0 (previously just
DurationSeconds > 0).
- Dropped-feature note renamed to 'indefinite timer duration (timer left
disabled)'; report friendly text updated. Status is still Partial — the
duration really is unknown.
(67731a3) ~John KauffmanNAG import: route phrase-scoped actions to their own phrase triggers
NAG actions target specific capture phrases via their "phrases" array (or a
single "phraseId"), but the import merged every non-timer action into one
shared set cloned onto all phrase nodes — so a multi-phrase trigger showed
the last-scoped action's sound/text/speech on every phrase, and one phrase's
audio replaced another's text. Unscoped actions still merge globally (last
wins, as before); scoped actions' display/speak/sound/share values now apply
only to their own phrase nodes (RouteActionValue -> PhraseScopedValues),
layered on top of the unscoped merge and only on each phrase's first timer
variant, matching the no-double-fire rule.
- Case 7 (clear variable) display-text routing is unified onto the same
mechanism; it now also honors single-field "phraseId" scoping like every
other action type.
- The "per-phrase action scoping" dropped-feature note now fires only for
action types without routing (timers, counters); routable actions instead
get "action targets phrase(s) missing from trigger" when their scope is
stale NAG data, with the values dropped rather than merged globally.
Sample DB: ~190 flagged triggers included every scoped text/audio/TTS
action; those are now routed correctly.
(e66198a) ~John KauffmanNAG import: keep the author's comment on every split trigger
A multi-timer (or multi-phrase) NAG trigger fans out into several EQLP
triggers, each a full trigger in its own right. The author's comment was
kept only on the first timer variant because it rode along with the shared
non-timer actions — but unlike those actions, a comment is inert metadata
that cannot double-fire, so every split node should carry it and stand on
its own. Test updated from pinning the old behavior to the new one.
(0612380) ~John KauffmanNAG import: honor case-sensitive end-early phrases
End-early phrase collection (trigger-level and per-timer-action) ignored the
per-phrase ignoreCase flag, so all 56 case-sensitive end-early phrases in the
sample DB were silently imported as case-insensitive (EQLP compiles every
regex pattern with IgnoreCase). Opted-out regex phrases now get the (?-i)
prefix like capture phrases; non-regex case-sensitive ones get a
dropped-feature note instead of being guessed at.
(c3613fa) ~John KauffmanNAG import: map timer ending/ended audio to warning/end sound slots
Timer actions' endingPlayAudioFileId and endedPlayAudioFileId were silently
dropped. EQLP has exactly matching slots (WarningSoundToPlay for the
ending-soon state, EndSoundToPlay at end), so map them with the same file
resolution and missing-file reporting as regular action audio.
Also extracts the duplicated id-resolution logic (files-database map,
'Speech ding' -> alert1.wav, existence check) into ResolveAudioFile, now
shared by action audio, timer end audio, and the dev-only scan.
(e7a8ee3) ~John KauffmanNAG import: map timer endingDuration to 'Warn With Time Remaining'
NAG timer actions carry endingDuration — the seconds before the end at which
the timer enters its ending state and warning text/sound fire. EQLP has the
matching WarningSeconds field (property grid: 'Warn With Time Remaining',
consumed by the timer runtime), but the import never read it, so all 326
affected timers in the sample DB lost their warning threshold.
Also adds an extraJson escape hatch to the test's action builder for fields
without a dedicated parameter.
(3d31128) ~John KauffmanDrop the UTF-8 BOM accidentally re-added to the solution file
(89d4e61) ~John KauffmanKeep GINA failure dialogs sequential and fix the misleading allowCancel name
Two review findings in the GINA platform seam:
1. ShowMessage was fire-and-forget (Action), so 'NextGinaTask' could run
while the error dialog was still open. The original WPF flow blocked on
ShowDialog() before scheduling the next task. ShowMessage is now
Func<string,string,Task> and GinaUtil awaits it; the host wiring awaits
the UI-thread dispatch.
2. The AskImportChoice bool was named allowCancel, but the original dialog
always had a Cancel button — the flag feeds MessageWindow's 'extra'
argument, which shows the auto-merge option row (originally passed
characterIds.Count > 0). Renamed to showMergeOption with a doc that says
what it actually does.
(b046e7c) ~John KauffmanNAG uniquify: skip suffixes already used by a literal sibling
When siblings are A, A, "A (2)", the old scheme renamed the duplicate to
"A (2)" — colliding with the existing node and re-introducing the same-name
sibling collision the uniquification exists to remove. Generated "Name (n)"
suffixes now step past any name already taken by a sibling. Also recurse into
null-named nodes so their children are still de-duplicated.
Adds a conversion test: A, A, "A (2)" -> A, A (2), A (3).
(9e4c1f9) ~John KauffmanMirror every accepted quick-share record into the WPF window's collection
The GINA flow moved to QuickShareState (Core) during the extraction, but
only the legacy share path still went through QuickShareManager — so
received GINA quick-shares were accepted into the state yet never
appeared in the QuickShare window (which binds solely to the manager's
collection).
QuickShareState now raises an Accepted event on the accepting thread
(after the lock, at most once per unique record) and QuickShareManager
subscribes once, mirroring accepted records into its bound collection on
the UI thread. Both producers (GINA via the state directly, legacy shares
via the manager) flow through that single mirror path; manager.Add is a
pass-through again.
(d5bb442) ~John KauffmanSilence nullable warnings in the new stats/data-store tests
Pure annotations: nullable event-capture locals, nullable Hit()
default args, string? CWD field, null! at the two parser callbacks
whose documented contract is 'return null for no match'.
(ab20693) ~John KauffmanAdd Linux-runnable tests for the FileSearcher log scanner
The last Utils file without any test coverage: a generic async
multi-file line scanner used by the search window, which posts
matched-line batches with stream positions and drives progress.
Four scenarios, each against temp files (CI-safe):
- start==0 scans any time; batches post in file-list order despite
parallel scanning, one batch per matching file, no-match files
post nothing
- timestamps outside the given TimeRange are skipped, and lines past
the last segment end abort that file's scan early
- a missing file completes silently and progress still reaches 100
- a full scan with zero matches posts nothing but reports progress
Notes pinned by the tests: parser returns (line, null) for non-matches
which the searcher converts to null; LinePosition values are buffered-read
positions so they are non-decreasing, not strictly increasing.
(6752673) ~John KauffmanAdd Linux-runnable tests for EQDataStore and ConfigUtil.ReadList
Eleven direct tests: ParseCustomSpellData column mapping (all 20 fields incl. duration ticks, resistance, ambiguity and lands-on text), rank-stripped abbreviations, short-line rejection; class registration through the host label hook (IsValidClassName/GetClassEnum/list); real-data lookups (healing filter by damaging<0 with a negative control, NPC/old-spell databases, parenthesized-title mapping); missing-file degradation (no throw, empty knowledge); AddUnknownSpell dedupe.
Also pins the ConfigUtil.ReadList separator regression: backslash data paths must resolve on non-Windows hosts, and missing files return empty without throwing.
(93e4b6d) ~John KauffmanAdd Linux-runnable tests for the healing, cast and misc line parsers
24 tests exercising Process(LineData) end-to-end: heal lines (direct/HoT/over-heal/self-heal pronoun normalization/negative), cast lines (you/others, old-style angled names, interrupt marking of the matching cast, zone-enter records), misc lines (corpse loot, master-looter and split currency copper conversion, resists, die rolls, mez breaks, need-roll wins, left-on-chest), and PreLineParser player/merc routing + FindPossiblePlayerName bounds/cross-server dots.
Two fixture quirks documented in the tests: zone lines are consumed before the cast pass so Process returns false; 'Targeted (Player)' carries two spaces. Behavior was not changed.
(37f844f) ~John KauffmanAdd Linux-runnable tests for the stats builders + formatter
Ten direct tests over DamageStatsBuilder/HealingStatsBuilder/TankingStatsBuilder/DamageOverlayStatsBuilder/StatsFormatter: raid totals, DPS rounding, rankings, pet aggregation (+Pets, percent-of-parent, Children map), MaxSeconds windowing, no-data (NONPC) event contract, healing per-healer/sub-stat breakdowns (including the PlayerRegistry name gate), tanking per-defender totals, overlay empty-state, and formatter line/title composition for DamageParse.
Notes from the work: builders group blocks with a lastTime==0 sentinel, so a block at exactly t==0 is dropped — real log timestamps never hit 0, left as-is. The healing test also exposed the first-heal off-by-one (fixed separately).
- Don't skip the first heal record when grouping
(e8f59b4) ~John KauffmanThe per-segment heal scan called FindIndex with a start index of 1, but FindIndex is 0-based — so the very first in-range heal was never examined and dropped from every full stats build. Off by one; now starts at 0.
(3ac5c4a) ~John KauffmanAdd Linux-runnable GINA/quick-share tests (chat detection, state rules, E2E import)
- CheckGina scenarios: trusted group chat adds a history record, tell is
addressed to the receiving character, own tells are marked IsMine, say
channel is recorded but auto-import ineligible — no network involved
(auto-processing stays gated behind TriggersWatchForQuickShare)
- QuickShareState rules in isolation: insert-once-at-top dedup + IsMine
(Snapshot added to the state for non-UI readers)
- End-to-end: a synthetic GINA zip package converts via GinaUtil and
imports into a temp TriggerStateDB through the same call GINA's flow
makes, asserting the folder/trigger land with their data
753/753 Core tests pass on Linux; full solution builds clean.
(9fdd3e5) ~John KauffmanMove the GINA download/convert/import flow into cross-platform Core
GinaUtil moves wholesale: XML conversion, the chunked SOAP download, the
chat-detection cache (CheckGina/ImportQuickShare) and the store import all
run in Core. The only WPF needs it had were windows:
- failure MessageWindows and the merge/new-folder question are replaced by
GinaPlatform hooks (ShowMessage + AskImportChoice); App.xaml.cs wires them
back to the same MessageWindow dialogs (same buttons, same allow-cancel
rule) on the UI thread
- the borrowed MainActions.TheHttpClient becomes a local client with
identical defaults; QuickShareManager.Instance is replaced by the Core
QuickShareState singleton from the previous commit
Behavior preserved: same download loop, same message texts, same import
calls, same NextGinaTask ordering. Callers (TriggerProcessor, TriggerUtil,
QuickShareWindow) now resolve GinaUtil through IVT unchanged.
(56f5e7c) ~John KauffmanMove quick-share domain (records, CharacterData, state rules) into Core
QuickShareRecord and CharacterData are plain data used by both the GINA
flow and the legacy share flow; the history dedup/ownership logic moves to
a thread-safe QuickShareState singleton so it is testable without WPF.
The WPF QuickShareManager becomes a thin UI adapter: same ObservableCollection
binding, same UI-thread marshaling of view updates, but the insert-once-at-top
decision now happens in the shared state. Behavior preserved.
(4c45aaf) ~John KauffmanRun the NAG conversion test suite on Linux + add real-dump round-trips
- NagUtilTriggerImportTest (80+ cases) moves out of Wpf.Test, including its
three NAG fixtures (now embedded resources of EQLogParser.Test; the
manifest-name lookup is suffix-based since RootNamespace differs)
- New NagStoreRealDataTest drives ConvertTriggers/ConvertOverlays against a
real NAG database dump under local/nag/ (gitignored, CI-safe skip) and
imports the result into a temp TriggerStateDB: conversion shape, import
idempotency, zero same-name sibling collisions, overlay round-trip
The real-dump run is what exposed the two import-fidelity fixes in the
previous commits. 747/747 Core tests pass on Linux.
- Uniquify same-name siblings when converting NAG databases
(b5c4d1e) ~John KauffmanReal NAG trigger databases contain multiple children with identical names
under one parent (e.g. several 'Fireball' triggers in one folder). The
trigger store keys children by (kind, name), so those duplicates merge into
or shadow each other — re-importing the same dump added more duplicates on
every pass and left indistinguishable siblings in the tree. Uniquifying at
the conversion boundary ('X', 'X' -> 'X', 'X (2)') matches the shape the
app's own .tgf exports already have, making NAG imports idempotent.
Verified by a real-dump round-trip test (next commit): import x2 is
stable and leaves zero same-name sibling collisions.
- Reset the audio-file map on every ConvertTriggers call
(3b14ef5) ~John KauffmanThe map was only rebuilt when a database directory was supplied, so a call
without one inherited the previous caller's map — stale fileId→path
resolution corrupted SoundToPlay and missing-audio tracking (surfaced by
running the NAG conversion tests in one process with a real files-database).
Production passes the same directory every time, so behavior is unchanged
there; the reset makes each call self-contained.
(510a237) ~John KauffmanMatch re-imported NAG triggers by OriginalId alone, not name+id
The importer does not rename same-name collisions, so with duplicate NAG
trigger names the stored sibling set contains identical names; the old
name+OriginalId match still worked for those — but combined with folder
merges of same-named folders (which the planner matches by name), re-imports
of real NAG databases accumulated duplicate children on every pass. OriginalId
is the stable source identity and survives on the stored node, so matching on
it alone is both more precise and resilient to any future rename behavior.
Covered by a new planner case (renamed existing node still matches by id)
and, from the next commit, by a real-dump re-import test.
(458a341) ~John KauffmanMove NagUtil (NAG trigger/overlay import) into cross-platform Core
Its only WPF coupling was two TriggerUtil.SoundFileExists calls, now
routed through the existing TriggerStorePlatform.SoundExists hook —
App.xaml.cs already wires that delegate to the same method, so this is
an identity swap. NagImportResult/NagTriggerMetadata travel with it.
The NAG import pipeline (ConvertTriggers/ConvertOverlays/skip reports)
is now testable on Linux, including against real NAG database dumps.
633/633 Core tests pass; WPF host and Wpf.Test compile clean.
(712a000) ~John KauffmanMove stats builders into Core, run validator tests on Linux
- control/builders: IStatsBuilder, DamageStatsBuilder, HealingStatsBuilder,
TankingStatsBuilder, DamageOverlayStatsBuilder, SpellCountBuilder, plus
StatsFormatter — all WPF-free; the one resx label use
(Resource.ANY_CLASS) routes through CombatRecordLookup.AnyClass
- DamageValidatorTest/HealingValidatorTest (44 tests) move out of
Wpf.Test; HealingValidatorTest wires the healing-spell seam to a real
EQDataStore so AOE rejection is tested against real spell data
Fixes a cross-platform bug the move exposed: ConfigUtil.ReadList passed
Windows-style paths (data\spells.txt) straight to File.Exists, which is
a literal name on Linux — every data file load silently returned empty,
so the spell/proc/NPC databases were never populated outside Windows.
ReadList now normalizes separators (no-op on Windows).
633/633 Core tests pass on Linux; WPF host and Wpf.Test compile clean.
(f547e31) ~John KauffmanRun the parser test suite on Linux (out of Wpf.Test)
Moves ChatLineParserTest, DamageLineParserTest and
LineModifiersParserTest (126 tests) from EQLogParser.Wpf.Test to
EQLogParser.Test so they execute on any OS:
- namespace rewritten to match the project
- test csproj copies the app's data/ files (spells.txt, npcs.txt, ...)
to the output where EQDataStore loads them
- Core gains IVT for DynamicProxyGenAssembly2 (Moq proxy generation of
internal IFightManager)
All 604 Core tests pass on Linux; WPF host and Wpf.Test still compile
clean.
(d4bc65b) ~John KauffmanMove parsing layer, EQDataStore and fight management into cross-platform Core
- All 9 parsing files (line parsers, ParserUtil, LineData,
ChatLineParser with its public ChatType/ChatChannels models)
- dao/store/EQDataStore (spell knowledge store). Its two resx lookups
route through a new CombatRecordLookup.ClassLabelByEnumName seam
wired in App.xaml.cs (identity to the previous ResourceManager call)
- control/managers: FightManager (+IFightManager), AdpsTracker, and
dao/store/RaidRosterStore — all BCL-only, pulled forward from later
phases because parsers/stores depend on them; hooking each call site
would have meant a dozen seams for types with zero WPF coupling
- Core csproj gains AllowUnsafeBlocks (ParserUtil's stackalloc join)
Pure relocation plus one seam, no behavior change. Core builds on
Linux; WPF host and Wpf.Test compile clean; 478/478 Core tests pass.
(c1c85d2) ~John KauffmanMove validators, stats util, record collections and session stores into Core
Follows the model split; everything moved is WPF-free (verified by usings):
- control/util: DamageValidator, HealingValidator, StatsUtil
- dao/util: RecordGroupCollections (+ nested RecordWrapper)
- util/AppSettings, parsing/LineModifiersParser
- dao/store: RecordsStore, PlayerRegistry (831 lines), plus the small
pure LifecycleManager registry they register with
- Extracted Labels, SpellTarget and SpellClass constants from
EQDataStore.cs into Core model files (verbatim).
EQDataStore stays in WPF for now; the handful of cross-cuts are routed
through a new CombatRecordLookup seam (healing spell lookup, player-spell
and class-name validation, class bitmask, resx class labels) wired in
App.xaml.cs — each entry is an identity delegate to the previous call
site. Utils gains IVT to Core for StringCache.
Pure relocation plus seam swaps, no behavior change. Core builds on
Linux; WPF host and Wpf.Test compile clean; 478/478 Core tests pass.
(7d35c30) ~John KauffmanMove combat log record and stats models into cross-platform Core project
Splits dao/model/DataModel.cs by dependency role (member-for-member,
verified line-level against the original):
- RecordModels.cs/StatsModel.cs into Core: IAction, hit/spell records,
timed actions, all *Record/*Event payloads, Fight, PlayerStats tree,
CombinedStats and related stats containers (INotifyPropertyChanged is
plain BCL, so these are platform-pure).
- Attempt.cs, parsing/SpellData.cs, parsing/HitRecord.cs moved whole.
- WPF keeps only the UI-facing types: IDocumentContent,
ComboBoxItemDetails, ParseData, PlayerStatsSelectionChangedEventArgs,
DataPointEvent (the last moves once its RecordGroupCollection does).
- LootRecord.Clone mapper moves to Core's ModelMapper; Riok.Mapperly
dropped from the WPF project.
- FightTable.xaml now resolves {x:Type core:Fight} via an explicit
clr-namespace/assembly xmlns (XAML does not search referenced
assemblies for unqualified local: types).
Pure relocation, no behavior change. Core builds on Linux; WPF host and
Wpf.Test compile clean; 478/478 Core tests pass.
(180e57a) ~John KauffmanAdd Linux-runnable store-level tests for TriggerStateDB (import, state, persistence)
(f76da18) ~John KauffmanMove TriggerStateDB and trigger store models into cross-platform Core project
(8cc1ff9) ~John KauffmanExtract trigger import matching into cross-platform TriggerImportPlanner with unit tests
(3964136) ~John KauffmanMove trigger store models into new cross-platform EQLogParser.Core project
(576aa82) ~John KauffmanFix re-import matching so same-named folder/trigger siblings can't erase each other
(3df9bc4) ~John KauffmanNAG import: skip malformed triggers instead of aborting and report overlay fidelity notes
(4ea5527) ~John KauffmanIgnore local nag sample data directory
(710b821) ~John KauffmanFix CS0103: zero DurationSeconds in per-node timer block else-branch
durationSeconds is a TimerActionData field, not a ParseTrigger local;
the timerless-zeroing belongs in the node loop's else branch instead.
(aa58201) ~John KauffmanFix EEP leak assertion and zero duration for timerless imports
The sibling-timer EEP test asserted the inverse of its own intent
(IsFalse on IsNullOrEmpty requires the leak). Separately, NAG
triggers without any timer action inherited the model's 0.2s
DurationSeconds default and exported a phantom timer; zero it out
at import time.
(3fbc8dd) ~John KauffmanAttach shared non-timer actions only to first timer variant
A NAG trigger's non-timer actions (text, TTS, audio, clipboard,
counters, set/clear variables) fire once per execution in NAG, but
every node of a multi-timer fan-out matches the same log line and
would re-run them (TTS spoken twice, counter incremented twice).
They now attach to each phrase's first timer variant only; sibling
variants carry just their own timer plus per-node import notes. The
NAG author's trigger comment rides with the shared actions.
(48719c4) ~John KauffmanIgnore pi-subagents session artifacts
(e5fffb1) ~John KauffmanIgnore local working documents directory
(caf0e18) ~John KauffmanFix NAG import: one node per timer action and route timer labels to AltTimerName
- ParseActions now collects one TimerActionData per NAG timer action (3/4/6/10)
instead of merging into shared locals where the last action overwrote earlier
ones' duration, label, and restart behavior.
- ParseTrigger fans out one node per capture phrase x timer action, named
'Name #i (Timer j)' when a trigger has multiple timers; non-timer actions
stay shared across nodes.
- Timer displayText maps to AltTimerName (the EQLP timer-bar label) instead of
TextToDisplay; text-overlay text keeps TextToDisplay.
- An action's own endEarlyPhrases merge into its timer variant's node list only
(max 3 slots, overflow reported as dropped).
- DotTimer (6) imports as a filling Progress timer and per-target types report
their lost per-target grouping in the import notes.
- Update real-data Bard Epic tests to the fan-out shape; add multi-timer,
label-routing, end-early scoping, and draw-direction tests.
Bug Fixes
(d8d14d2) ~John KauffmanFix stale action type 6 label in code comment
(ada2a23) ~John KauffmanFix negated equality double parens and stale nag test expectations
(3dd4e3b) ~John KauffmanFix tests
(f9550bf) ~John KauffmanFix multiple phrase trigger
(adec5da) ~John KauffmanFix import multiple phrases
- Phrase-specific clear-variable (actionType 7) creates VariableAction
(46943c2) ~John KauffmanNAG actionType 7 with a phraseId means 'when this specific phrase matches,
clear the variable'. Previously, all clear-variable actions were mapped to
EndTimerClearVariables, which only fires when a timer ends. For triggers
like 'Capture spell casting' where the clear action has no co-occurring
timer, EndTimerClearVariables would never fire — leaving SpellBeingCast
uncleared when a spell is interrupted.
- Skip blank template actions with missing actionType
(7ac14f3) ~John KauffmanNAG data contains empty template action objects with no actionType field
and all null/default values. These were falling through to the default
switch case and being logged as 'action type -1' dropped features,
causing valid triggers to be marked as Partial. Now skipped silently.
(c84aec8) ~John KauffmanFix variable format tests
(8b92d1e) ~John KauffmanFix MatchVariableCondition description to plain sentence form
Remove \n escape sequences that display literally in the UI. Keep
description as a single flowing sentence consistent with other
property grid descriptions.
(923536d) ~John KauffmanFix NAG pipe-separated contains conditions not matching
NAG uses pipe-separated values in contains conditions (e.g.
{SpellBeingCast} contains "Fireball|Flame Strike") to mean 'contains
any of'. But EQLP's contains operator does a literal substring check,
so the entire string 'Fireball|Flame Strike' was being searched for as
a single value — which never matches.
Fix: split on | and create separate contains clauses joined by ||,
e.g. {SpellBeingCast} contains "Fireball" || {SpellBeingCast} contains
"Flame Strike". Single values (no pipe) remain unchanged.
Updated existing test to use single value; added new test for
pipe-separated multi-value case.
(4b3372e) ~John KauffmanFix NAG trigger overlay assignments lost during import
Root cause: triggers were imported BEFORE overlays, so
ValidateOverlays() in TriggerStateDB.Import() found no matching
overlay nodes and stripped all SelectedOverlays references to [].
Fix: swap import order — call ImportNagOverlays() before
ImportTriggers() so that overlay nodes exist in the tree when
triggers are validated.
(9eba9aa) ~John KauffmanFix TextOverlayWrap not updating live when changed in property grid
The ValueChanged handler updates Application.Current.Resources for each
overlay property as the user edits it, but was missing a case for
TextOverlayWrap. This meant changing 'Text Wrap' in the property grid
updated the data model but didn't update the resource dictionary that
the TextBlocks are bound to via SetResourceReference.
Added noTextWrapItem.PropertyName case that updates
TextOverlayTextWrapping-{node.Id} resource and enables save button,
following the same pattern as fontFamilyItem, fontWeightItem, etc.
(76dafc7) ~John KauffmanFix TextOverlayWrap not updating when changed via property grid
Root cause: CreateBlock() set TextWrapping as a local value from
NoTextWrap, so changing it in the property grid didn't propagate to
existing TextBlocks. Other overlay properties (font family, size, etc.)
work because they use SetResourceReference with named resources that
get updated in Application.Current.Resources.
Fix:
- Rename NoTextWrap → TextOverlayWrap (default true = wrap enabled)
across data model, NagUtil parsing, TriggerUtil sync, property grid,
and tests
- Add TextOverlayTextWrapping-{node.Id} resource reference pattern
following the same approach as TextOverlayFontFamily etc.
- Set the resource in TriggerUtil.Copy() when loading/syncing overlays
- Use SetResourceReference(TextBlock.TextWrappingProperty, ...) in
CreateBlock() instead of direct local value assignment
- Rename property grid display from 'No Text Wrap' to 'Text Wrap'
(default on)
- Update NagUtil tests: NoTextWrap → TextOverlayWrap, assertions
inverted (nowrap → false, default → true)
(9a88fd8) ~John KauffmanFix backup filename showing 1.0.0 instead of real version
typeof(FileUtil).Assembly returns EQLogParser.Utils.dll (always 1.0.0)
instead of the main EQLP executable. Use Assembly.GetEntryAssembly() to
get the actual application assembly, falling back to typeof(FileUtil).Assembly
when null (e.g., in test contexts).
(5e255bc) ~John KauffmanFix NAG trigger folder hierarchy lost during import
ConvertTriggers returned a flat list of folder-wrapped nodes, but the
first Import() overload strips the top-level wrapper (designed for GINA's
single root node). This caused single-level folders like 'Orphaned Triggers'
to lose their folder entirely, and multi-level folders like 'Raids/...' to
lose their top-level 'Raids' parent.
Fix: wrap all NAG trigger nodes in a root ExportTriggerNode, consistent with
GINA's export format, so the first Import() correctly skips the root and
processes each folder/trigger node through the second Import() overload.
(49d4395) ~John KauffmanFix NAG overlay imports being silently skipped
ConvertOverlays returns flat leaf nodes (OverlayData, no child Nodes),
but the first Import() overload only recursed into nodes with child Nodes.
Added else-if branch to process overlay leaf nodes directly via the second
Import() overload which handles OverlayData.
(885d39e) ~John KauffmanFix import boolean NAG
(caf6a57) ~John KauffmanFix for correcting missing path
(0588b01) ~John KauffmanFix to avoid opening file chooser when selecting file programatically
(d847fdf) ~John KauffmanFix test
(846ebd7) ~John KauffmanFix alt timername and custom variables
(e6313bd) ~John KauffmanFix style issues and improved locking
(b683819) ~John KauffmanFix trigger tester
(3cd8171) ~John KauffmanFix tests
(e74f100) ~John KauffmanFix typo
(641efa0) ~John KauffmanFix resource leak and added tests
(2b714f0) ~John KauffmanFix for end clear variable not saving
(cbc3219) ~John KauffmanFix description for end clear variables
(a587729) ~John KauffmanFix formatting
(dbdd7ab) ~John KauffmanFix variable TTS related to timeout
(de2dad7) ~John KauffmanFix variable expiration
(e6d5f50) ~John KauffmanFix loading variable settings
(ae1e351) ~John KauffmanFixed matches variable validator
(ef6f33b) ~John KauffmanFix for text value being cut short
(c29c418) ~John KauffmanFix event handling and more UI updates
Documentation
(03437d1) ~John KauffmanDoc updates
(0b13cb6) ~John KauffmanDoc updates
(7a04dd4) ~John KauffmanDoc updates
Performance
(81d9d24) ~John KauffmanPerformance cleanup
(9aa7e9c) ~John KauffmanPerformance updates for variables
Styling
(2476c7f) ~John KauffmanStyle cleanup
Testing
(9160985) ~John KauffmanTest updates
(458b326) ~John KauffmanTest fixes
(6d7332b) ~John KauffmanTest updates
Commits
(3b5b72f) ~John KauffmanV2.3.60
(b8ed54c) ~John KauffmanHide tree expander gutter when there are no folders
(e8e6c0b) ~John KauffmanAdd vertical breathing room around character toolbar buttons
(18cf670) ~John KauffmanMatch manage characters title bar to triggers height
(549a05e) ~John KauffmanSize manage characters header to its buttons
(73f9efa) ~John KauffmanApply character panel width on theme change
(62878f6) ~John KauffmanWiden character panel a bit more at large fonts
(a238de1) ~John KauffmanUnify toolbar button heights and zero new button padding
(0d395f4) ~John KauffmanWiden character panel at large font sizes
(5fa204f) ~John KauffmanHide new button icon and tighten button row spacing
(5d49c53) ~John KauffmanMake toolbar new dropdown always create at top level
(bb8b2d8) ~John KauffmanReplace character add button with new dropdown
(ab80d31) ~John KauffmanWrite character status onto live tree instances
(c235ee7) ~John KauffmanUse warning icon and unquoted name in delete confirmations
(6d68d7f) ~John KauffmanSync character folder checkboxes with child states
(490e806) ~John KauffmanRevert "shrink message window for short text"
This reverts commit commit # [099ec8b](https://github.com/kauffman12/EQLogParser/commit/099ec8b0d5e2c6fdb2e938a3b5f93982dd174925).
(06bee1e) ~John KauffmanFolder delete back to one line with warning icon
(7dc9853) ~John KauffmanPut folder delete note on its own line
(2c0c28f) ~John KauffmanFolder delete message says contents move to parent
(58d5e44) ~John KauffmanTighten folder delete contents wording
(72dd6d9) ~John KauffmanDrop folder word from delete confirmation
(099ec8b) ~John KauffmanShrink message window for short text
(1fb0f29) ~John KauffmanMatch delete confirmations to app wording and question icon
(50dbd8b) ~John KauffmanReplace character toolbar with compact regular buttons
(33ce076) ~John KauffmanFrame manage characters panel like triggers view header
(9b896eb) ~John KauffmanRevert "give character tree context menus an elevated surface"
This reverts commit commit # [4ba5a82](https://github.com/kauffman12/EQLogParser/commit/4ba5a820333b82e00860ff3df6b8d8f43aa01fa3).
(4ba5a82) ~John KauffmanGive character tree context menus an elevated surface
(c481535) ~John KauffmanAdd new character option to character tree context menus
(db15e27) ~John KauffmanRemoved null annotation
(22a3962) ~John KauffmanStop importing nag text duration as countdown
(61a93fb) ~John KauffmanNag import fixes for operators, case sensitivity and drop notes
(a6b3ab3) ~John KauffmanAdd character folder system and f2 root-rename guard
(eaeb128) ~John KauffmanLatest
(2e29457) ~John KauffmanNag fixes
(920c261) ~John KauffmanNag improvements
(ad31f3a) ~John KauffmanUpdated migrate nag database menu
(4c164e6) ~John KauffmanMore updates to triggers with multiple capture phrases
(ea3463a) ~John KauffmanCode cleanup
(5a23a37) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(339a461) ~John KauffmanMore test updates
(4ed885d) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(b72fb75) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(1a60c1a) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(677e40a) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(3e93e2d) ~John KauffmanMoved import NAG DB feature
(a82845a) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(d0f20c2) ~John KauffmanAdded fallback for phrase actions
(ac072aa) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(5c6158c) ~John KauffmanCleanup
(38f08ae) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(5d08038) ~John KauffmanImplent nag conunters
(3f31cfd) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(7b211f3) ~John KauffmanAdded unit tests for import from nag
(0bb0d16) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(a7014ba) ~John KauffmanAdd user-friendly descriptions for dropped features in HTML report
Replace cryptic feature names like 'set variable (BrdEpic1.5Caster)'
with plain-language explanations:
- set variable: explains that EQLP requires regex named groups
instead, and the trigger still works but stored values may be empty
- class level filtering: explains EQLP doesn't support this
- Other features fall back to their original name
This makes the Partial status reason column much more useful for
users who don't know NAG internals.
(3c25f95) ~John KauffmanInclude variable name in dropped feature reason for unsupported actions
When an action type like 'set variable' (type 5) is dropped, include
the variable name in the reason string so users can identify which
specific variable was involved. E.g. 'set variable (BrdEpic1.5Caster)'
instead of just 'set variable'.
(d0c5da4) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(c098160) ~John KauffmanMap NAG clear variable actions to EndTimerClearVariables
NAG actionType 7 ('clear variable') was being dropped as an unsupported
feature, marking triggers as Partial. EQLP already supports clearing
variables when a timer ends via the EndTimerClearVariables field.
Now maps actionType 7's variableName directly to EndTimerClearVariables,
so triggers like 'Vainglorious Shout VIII' that clear SpellBeingCast
on timer end will work correctly instead of being marked Partial.
Added test: ConvertTriggers_ActionType7_ClearVariable_MappedToEndTimerClearVariables
(952ca5b) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(9d8e1fa) ~John KauffmanMark triggers with missing audio files as Partial instead of Imported
Triggers that import successfully but reference sound files not found
on disk should be 'Partial' (imported but incomplete), not 'Imported'.
This surfaces them in the HTML report's status sorting (Skipped →
Partial → Success) so users notice they need to locate the missing
files. Also adds a reason string listing the count of missing files.
(f827ee2) ~John KauffmanSimplify NAG import report filename and dialog label
- Rename HTML report from 'eqlp-import-report-yyyy-MM-dd_HH-mm-ss.html'
to just 'nag-import.html' in the log directory. The timestamped name
was unnecessarily long; a simple fixed name is cleaner.
- Change import dialog heading from 'Detailed HTML Report:' to 'Report:'
(9c47a5f) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(b070ce6) ~John KauffmanCleanup NAG import dialog and highlight missing audio files
- Remove the list of missing audio files from the import dialog.
People can check the HTML report for that info. This keeps the
dialog concise instead of wasting space with potentially dozens
of file paths.
- Change missing audio file styling in HTML report from grey
(#999) to bold red (#b71c1c) so they stand out as issues.
(05ada98) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(de1ef31) ~John KauffmanCleanup MatchVariableCondition description in triggers view
The old description was 556 chars with excessive syntax documentation,
operator lists, and literal \n sequences that showed as text in the UI.
Replaced with a brief description plus concise examples using == and >,
and a pointer to the documentation.
(69587e1) ~John KauffmanMap NAG timerBackgroundColor to EQLP IdleColor
NAG timer overlays have separate colors for the active portion
(timerColor) and the background/track (timerBackgroundColor), but
only timerColor was being mapped. The timer bar track defaulted to
red (#FF8f1515) instead of the NAG-specified color.
Now maps timerBackgroundColor → IdleColor, preserving the original
dark brown (rgba(67,62,16,0.75)) for overlays like 'Quickies'.
(c1b24b5) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(eff63fa) ~John KauffmanShorten NAG folder dialog description to prevent text wrapping
The FolderBrowserDialog wraps long descriptions awkwardly (every few
words). Shortened from 'Select the directory containing the NAG database
files (overlays-database.json, etc.)' to 'Select the directory containing
your NAG database files.'
(f2e04c6) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(d5ec8cf) ~John KauffmanSimplify NAG import progress dialog text
(ff1d88c) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(9fb914a) ~John KauffmanDark theme + sort HTML report by status, rename Imported to Success
- Apply dark theme CSS (VS-style) to HTML import report:
dark backgrounds (#1e1e1e, #2d2d2d), cyan headers (#4fc3f7),
adjusted badge colors for dark mode readability
- Sort report rows by status: Skipped first, then Partial, then Success
- Rename 'Imported' to 'Success' in report display text (badge labels and
summary stats); internal Status values remain unchanged
- Add WriteImportReportHtml_ResultsSortedByStatus_SkippedFirst test verifying
sort order
- Update existing HTML tests: check for 'Success' instead of 'Imported',
verify dark theme CSS is present
(04428be) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(829476a) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(694cfad) ~John KauffmanStore HTML import report in log directory with timestamp + add Open Report button
- Save HTML report only in the EQLP log directory (same dir as error log),
not in the NAG database directory, to avoid polluting user data
- Timestamped filename: eqlp-import-report-yyyy-MM-dd_HH-mm-ss.html
- Added GetLogDirectory() helper that reads the log4net FileAppender path
- Added 'Open Report' button to the NAG Import Complete dialog that opens
the HTML report in the default web browser
(75d8857) ~John KauffmanRemove CSV import report, keep HTML only
The CSV report (WriteImportReport + EscapeCsv) had no external consumers
and the HTML report is strictly more capable. Removed from NagUtil.cs
and TriggerUtil.cs; updated tests to use WriteImportReportHtml instead.
(3f0bc19) ~John KauffmanUpdate NagUtil tests for comment format + add HTML report tests
- Update ConvertTriggers_Comments_PreservedInNagComment to assert that
'Original:' prefix is NOT present (removed in commit # [3b09528](https://github.com/kauffman12/EQLogParser/commit/3b09528a))
- Update ConvertTriggers_DroppedFeatures_ListedInComment to check for
'EQLP Import Notes:' instead of 'Dropped:' (renamed in commit # [3b09528](https://github.com/kauffman12/EQLogParser/commit/3b09528a))
- Add WriteImportReportHtml_ValidResults_CreatesHtml — verifies HTML
structure, summary stats, trigger names, folder paths, missing audio
files, and badge CSS classes
- Add WriteImportReportHtml_EmptyResults_CreatesHtml — verifies graceful
handling of empty results list
- Add WriteImportReportHtml_SpecialCharacters_AreHtmlEncoded — verifies
HTML encoding of < > & characters in trigger names
- Add WriteImportReportHtml_RootFolder_ShowsEmRoot — verifies root folder
path displays as <em>(root)</em>
(860c6ba) ~John KauffmanAdd HTML import report alongside CSV
Adds WriteImportReportHtml() which generates a styled HTML report with
summary stats (imported/partial/skipped counts), color-coded status badges,
folder path display, and expandable missing audio file listings. The
report is generated as eqlp-import-report.html in the NAG database
directory, and both paths are shown in the import summary dialog.
(08d0832) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(e212238) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(0d106b9) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(846b6a1) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(3b09528) ~John KauffmanChanged import item
(d2410c2) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(610503a) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(53f85ab) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(636aa1a) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(398d946) ~John KauffmanMerge branch 'master' into develop
(4cc3784) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(7d2acc6) ~John KauffmanUpdated custom audio selection
(162697d) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(cabf1c1) ~John KauffmanCode cleanup
(65206d3) ~John KauffmanHandle custom audio files
(4487022) ~John KauffmanImport nag v1
(c53623e) ~John Kauffman2.3.58
(a37882a) ~John KauffmanUpdate timer variable performance
(9b5c077) ~John KauffmanResolve _variables last and make timer updates for them dynamic
(25e66c5) ~John Kauffman2.3.57
(2f282ac) ~John KauffmanAdded trigger variables
(cf53fde) ~John KauffmanReview fixes
(9df3f06) ~John KauffmanUpdated dialog and docs
(fc5032d) ~John KauffmanCleanup
(2957fd5) ~John KauffmanBug fixes
(c756dac) ~John KauffmanAdded progress dialog for import
(d588a5d) ~John KauffmanCleanup
(974ad5d) ~John KauffmanAdded variable clear when timer ends
(c2ad0a1) ~John KauffmanAdded command to clear variables
(07af1fc) ~John KauffmanChanged to Fixed
(21bb950) ~John KauffmanRenamed Value option
(dbbaf7b) ~John KauffmanChanged internal types to number
(808f8b3) ~John KauffmanRenamed Text to Value
(4836f14) ~John KauffmanUpdated docs
(f050ff3) ~John KauffmanMade variable condition validation smarter
(792a793) ~John KauffmanAdd validation for match variables field
(5dcfb57) ~John KauffmanCleanup
(e4e6dec) ~John KauffmanAdded trigger variables
(3deff69) ~John KauffmanUpdated tab names and fixes
(5925a72) ~John KauffmanAdded TTL to variables and updated style
(41e3a04) ~John KauffmanUI cleanup and model updates
(562f50d) ~John KauffmanUI update for variables tab on trigger properties
(0ceb2c0) ~John KauffmanV2.3.56
(d2ff09a) ~John KauffmanV2.3.55
(be43d92) ~John KauffmanExpand all levels
(9600886) ~John KauffmanHandle days when displaying timer
(f6b5ff2) ~John KauffmanMerge branch 'master' into picode
(8e929e8) ~kauffman12PR # [392](https://github.com/kauffman12/EQLogParser/pull/392): text/timer overlay backgrounds rendering black on Wayland/Wine
(ef09d57) ~John KauffmanExtend {ts} labeled formats, add tree grid expand/collapse, and broaden trigger search
- Support labeled time formats in {ts} trigger tag (e.g. 4h:20m:53s, 1d:10h:20m:50s)
with optional components and flexible ordering. Update UpdateTimePattern regex
and refactor SimpleTimeToSeconds to detect and parse d/h/m/s suffixes.
- Add "Expand All" and "Collapse All" context menu items to all SfTreeGrid views
(Damage/Healing/Tanking Breakdown, Damage Summary, Taunt Stats, Random Viewer).
Commands are enabled only on SfTreeGrid targets with nodes.
- Extend trigger search to match against Comments, PreviousPattern,
EndEarlyPattern (1-3), and AltTimerName in addition to Name and Pattern.
(ee7cdf8) ~John KauffmanMerge branch 'master' into picode
(16d351a) ~stvsuFix text/timer overlay backgrounds rendering black on Wayland/Wine
The text and timer overlays are AllowsTransparency windows that fill their
background and border with a fully-transparent brush (#00000000, alpha 0).
On wlroots-based Wayland compositors (e.g. Hyprland) running under Wine/Proton,
fully-transparent regions are not alpha-blended and render as opaque black,
while areas covered by drawn content (timer bars, text) composite correctly.
Windows and KDE/KWin are not affected.
Give the background and border a minimal non-zero alpha (#01000000, 1/255) so
the whole surface goes through the normal blend path. The change is visually
imperceptible and produces identical output on Windows and KDE.
(8a78009) ~kauffman12PR # [389](https://github.com/kauffman12/EQLogParser/pull/389): Add support for 'cleave' 'reave' and 'smite' for DPS parser
Bug Fixes
(223c1ba) ~John KauffmanFix stale action type 6 label (dottimer) in nag docs and comment
(62210d8) ~John KauffmanFix negated equality double parens and stale nag test expectations
Commits
(ea9b623) ~John KauffmanV2.3.60
(9e2ebf3) ~John KauffmanHide tree expander gutter when there are no folders
(3eee461) ~John KauffmanAdd vertical breathing room around character toolbar buttons
(996724a) ~John KauffmanMatch manage characters title bar to triggers height
(6d3e9e5) ~John KauffmanSize manage characters header to its buttons
(b58dba9) ~John KauffmanApply character panel width on theme change
(cc4f8ff) ~John KauffmanWiden character panel a bit more at large fonts
(0a2623e) ~John KauffmanUnify toolbar button heights and zero new button padding
(3b73362) ~John KauffmanWiden character panel at large font sizes
(6df6070) ~John KauffmanHide new button icon and tighten button row spacing
(69ff2a4) ~John KauffmanMake toolbar new dropdown always create at top level
(ea5ff6f) ~John KauffmanReplace character add button with new dropdown
(c41fa25) ~John KauffmanWrite character status onto live tree instances
(c501275) ~John KauffmanUse warning icon and unquoted name in delete confirmations
(0ec1788) ~John KauffmanSync character folder checkboxes with child states
(57014de) ~John KauffmanRevert "shrink message window for short text"
This reverts commit commit # [6eddaf7](https://github.com/kauffman12/EQLogParser/commit/6eddaf77d513e98490241635fdd4462cc8115a42).
(762b2eb) ~John KauffmanFolder delete back to one line with warning icon
(79c817b) ~John KauffmanPut folder delete note on its own line
(08f8e55) ~John KauffmanFolder delete message says contents move to parent
(9aa9b0d) ~John KauffmanTighten folder delete contents wording
(bc142c8) ~John KauffmanDrop folder word from delete confirmation
(6eddaf7) ~John KauffmanShrink message window for short text
(9f6fdc9) ~John KauffmanMatch delete confirmations to app wording and question icon
(4671b4f) ~John KauffmanReplace character toolbar with compact regular buttons
(c8ce6c0) ~John KauffmanFrame manage characters panel like triggers view header
(4e56a7c) ~John KauffmanRevert "give character tree context menus an elevated surface"
This reverts commit commit # [7009a04](https://github.com/kauffman12/EQLogParser/commit/7009a0444d05e043dc87777f87d83b4b9d23bdb4).
(7009a04) ~John KauffmanGive character tree context menus an elevated surface
(d2dd30f) ~John KauffmanAdd new character option to character tree context menus
(a1b92a9) ~John KauffmanRemoved null annotation
(cbf11f2) ~John KauffmanStop importing nag text duration as countdown
(5372ef7) ~John KauffmanNag import fixes for operators, case sensitivity and drop notes
(7afeb84) ~John KauffmanAdd character folder system and f2 root-rename guard
Bug Fixes
(88f9ca1) ~John KauffmanFix tests
(cbd56ed) ~John KauffmanFix multiple phrase trigger
(a64b5e7) ~John KauffmanFix import multiple phrases
- Phrase-specific clear-variable (actionType 7) creates VariableAction
(735842c) ~John KauffmanNAG actionType 7 with a phraseId means 'when this specific phrase matches,
clear the variable'. Previously, all clear-variable actions were mapped to
EndTimerClearVariables, which only fires when a timer ends. For triggers
like 'Capture spell casting' where the clear action has no co-occurring
timer, EndTimerClearVariables would never fire — leaving SpellBeingCast
uncleared when a spell is interrupted.
- Skip blank template actions with missing actionType
(80670fd) ~John KauffmanNAG data contains empty template action objects with no actionType field
and all null/default values. These were falling through to the default
switch case and being logged as 'action type -1' dropped features,
causing valid triggers to be marked as Partial. Now skipped silently.
(305f0ab) ~John KauffmanFix variable format tests
(ad914e5) ~John KauffmanFix MatchVariableCondition description to plain sentence form
Remove \n escape sequences that display literally in the UI. Keep
description as a single flowing sentence consistent with other
property grid descriptions.
(4c16c94) ~John KauffmanFix NAG pipe-separated contains conditions not matching
NAG uses pipe-separated values in contains conditions (e.g.
{SpellBeingCast} contains "Fireball|Flame Strike") to mean 'contains
any of'. But EQLP's contains operator does a literal substring check,
so the entire string 'Fireball|Flame Strike' was being searched for as
a single value — which never matches.
Fix: split on | and create separate contains clauses joined by ||,
e.g. {SpellBeingCast} contains "Fireball" || {SpellBeingCast} contains
"Flame Strike". Single values (no pipe) remain unchanged.
Updated existing test to use single value; added new test for
pipe-separated multi-value case.
(c35d4ff) ~John KauffmanFix NAG trigger overlay assignments lost during import
Root cause: triggers were imported BEFORE overlays, so
ValidateOverlays() in TriggerStateDB.Import() found no matching
overlay nodes and stripped all SelectedOverlays references to [].
Fix: swap import order — call ImportNagOverlays() before
ImportTriggers() so that overlay nodes exist in the tree when
triggers are validated.
(456f9cb) ~John KauffmanFix TextOverlayWrap not updating live when changed in property grid
The ValueChanged handler updates Application.Current.Resources for each
overlay property as the user edits it, but was missing a case for
TextOverlayWrap. This meant changing 'Text Wrap' in the property grid
updated the data model but didn't update the resource dictionary that
the TextBlocks are bound to via SetResourceReference.
Added noTextWrapItem.PropertyName case that updates
TextOverlayTextWrapping-{node.Id} resource and enables save button,
following the same pattern as fontFamilyItem, fontWeightItem, etc.
(5fd7e9b) ~John KauffmanFix TextOverlayWrap not updating when changed via property grid
Root cause: CreateBlock() set TextWrapping as a local value from
NoTextWrap, so changing it in the property grid didn't propagate to
existing TextBlocks. Other overlay properties (font family, size, etc.)
work because they use SetResourceReference with named resources that
get updated in Application.Current.Resources.
Fix:
- Rename NoTextWrap → TextOverlayWrap (default true = wrap enabled)
across data model, NagUtil parsing, TriggerUtil sync, property grid,
and tests
- Add TextOverlayTextWrapping-{node.Id} resource reference pattern
following the same approach as TextOverlayFontFamily etc.
- Set the resource in TriggerUtil.Copy() when loading/syncing overlays
- Use SetResourceReference(TextBlock.TextWrappingProperty, ...) in
CreateBlock() instead of direct local value assignment
- Rename property grid display from 'No Text Wrap' to 'Text Wrap'
(default on)
- Update NagUtil tests: NoTextWrap → TextOverlayWrap, assertions
inverted (nowrap → false, default → true)
(74495bb) ~John KauffmanFix backup filename showing 1.0.0 instead of real version
typeof(FileUtil).Assembly returns EQLogParser.Utils.dll (always 1.0.0)
instead of the main EQLP executable. Use Assembly.GetEntryAssembly() to
get the actual application assembly, falling back to typeof(FileUtil).Assembly
when null (e.g., in test contexts).
(4778814) ~John KauffmanFix NAG trigger folder hierarchy lost during import
ConvertTriggers returned a flat list of folder-wrapped nodes, but the
first Import() overload strips the top-level wrapper (designed for GINA's
single root node). This caused single-level folders like 'Orphaned Triggers'
to lose their folder entirely, and multi-level folders like 'Raids/...' to
lose their top-level 'Raids' parent.
Fix: wrap all NAG trigger nodes in a root ExportTriggerNode, consistent with
GINA's export format, so the first Import() correctly skips the root and
processes each folder/trigger node through the second Import() overload.
(9c8668c) ~John KauffmanFix NAG overlay imports being silently skipped
ConvertOverlays returns flat leaf nodes (OverlayData, no child Nodes),
but the first Import() overload only recursed into nodes with child Nodes.
Added else-if branch to process overlay leaf nodes directly via the second
Import() overload which handles OverlayData.
(5bbcb01) ~John KauffmanFix import boolean NAG
(1a766f2) ~John KauffmanFix for correcting missing path
(d06c203) ~John KauffmanFix to avoid opening file chooser when selecting file programatically
Documentation
(0b47b5d) ~John KauffmanDoc updates
(32976fc) ~John KauffmanDoc updates
(02e9027) ~John KauffmanDoc updates
Testing
(c440e78) ~John KauffmanTest updates
(f8924a4) ~John KauffmanTest fixes
(c12d4d8) ~John KauffmanTest updates
Commits
(8188587) ~John KauffmanLatest
(28c9550) ~John KauffmanNag fixes
(44b1b45) ~John KauffmanNag improvements
(5ad4a8f) ~John KauffmanUpdated migrate nag database menu
(c2e39dc) ~John KauffmanMore updates to triggers with multiple capture phrases
(66af5be) ~John KauffmanCode cleanup
(bbdaf7f) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(a31d7e7) ~John KauffmanMore test updates
(e02ee6c) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(2e37f87) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(d3061b3) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(9beb805) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(0ebd0e3) ~John KauffmanMoved import NAG DB feature
(2a4eaf4) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(4a3b4e3) ~John KauffmanAdded fallback for phrase actions
(685f960) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(6594de1) ~John KauffmanCleanup
(e7de56c) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(ec41d64) ~John KauffmanImplent nag conunters
(47ff80a) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(952d75e) ~John KauffmanAdded unit tests for import from nag
(1e6e7d3) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(b433d95) ~John KauffmanAdd user-friendly descriptions for dropped features in HTML report
Replace cryptic feature names like 'set variable (BrdEpic1.5Caster)'
with plain-language explanations:
- set variable: explains that EQLP requires regex named groups
instead, and the trigger still works but stored values may be empty
- class level filtering: explains EQLP doesn't support this
- Other features fall back to their original name
This makes the Partial status reason column much more useful for
users who don't know NAG internals.
(55be2bf) ~John KauffmanInclude variable name in dropped feature reason for unsupported actions
When an action type like 'set variable' (type 5) is dropped, include
the variable name in the reason string so users can identify which
specific variable was involved. E.g. 'set variable (BrdEpic1.5Caster)'
instead of just 'set variable'.
(ddbf4e7) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(4074ce3) ~John KauffmanMap NAG clear variable actions to EndTimerClearVariables
NAG actionType 7 ('clear variable') was being dropped as an unsupported
feature, marking triggers as Partial. EQLP already supports clearing
variables when a timer ends via the EndTimerClearVariables field.
Now maps actionType 7's variableName directly to EndTimerClearVariables,
so triggers like 'Vainglorious Shout VIII' that clear SpellBeingCast
on timer end will work correctly instead of being marked Partial.
Added test: ConvertTriggers_ActionType7_ClearVariable_MappedToEndTimerClearVariables
(9114585) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(712682f) ~John KauffmanMark triggers with missing audio files as Partial instead of Imported
Triggers that import successfully but reference sound files not found
on disk should be 'Partial' (imported but incomplete), not 'Imported'.
This surfaces them in the HTML report's status sorting (Skipped →
Partial → Success) so users notice they need to locate the missing
files. Also adds a reason string listing the count of missing files.
(514ffe7) ~John KauffmanSimplify NAG import report filename and dialog label
- Rename HTML report from 'eqlp-import-report-yyyy-MM-dd_HH-mm-ss.html'
to just 'nag-import.html' in the log directory. The timestamped name
was unnecessarily long; a simple fixed name is cleaner.
- Change import dialog heading from 'Detailed HTML Report:' to 'Report:'
(264ffaa) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(0b14879) ~John KauffmanCleanup NAG import dialog and highlight missing audio files
- Remove the list of missing audio files from the import dialog.
People can check the HTML report for that info. This keeps the
dialog concise instead of wasting space with potentially dozens
of file paths.
- Change missing audio file styling in HTML report from grey
(#999) to bold red (#b71c1c) so they stand out as issues.
(2b23de7) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(dc6807a) ~John KauffmanCleanup MatchVariableCondition description in triggers view
The old description was 556 chars with excessive syntax documentation,
operator lists, and literal \n sequences that showed as text in the UI.
Replaced with a brief description plus concise examples using == and >,
and a pointer to the documentation.
(04e70d7) ~John KauffmanMap NAG timerBackgroundColor to EQLP IdleColor
NAG timer overlays have separate colors for the active portion
(timerColor) and the background/track (timerBackgroundColor), but
only timerColor was being mapped. The timer bar track defaulted to
red (#FF8f1515) instead of the NAG-specified color.
Now maps timerBackgroundColor → IdleColor, preserving the original
dark brown (rgba(67,62,16,0.75)) for overlays like 'Quickies'.
(5b4d39e) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(ed2e479) ~John KauffmanShorten NAG folder dialog description to prevent text wrapping
The FolderBrowserDialog wraps long descriptions awkwardly (every few
words). Shortened from 'Select the directory containing the NAG database
files (overlays-database.json, etc.)' to 'Select the directory containing
your NAG database files.'
(df75bd9) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(56f6b39) ~John KauffmanSimplify NAG import progress dialog text
(f682441) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(b93a52d) ~John KauffmanDark theme + sort HTML report by status, rename Imported to Success
- Apply dark theme CSS (VS-style) to HTML import report:
dark backgrounds (#1e1e1e, #2d2d2d), cyan headers (#4fc3f7),
adjusted badge colors for dark mode readability
- Sort report rows by status: Skipped first, then Partial, then Success
- Rename 'Imported' to 'Success' in report display text (badge labels and
summary stats); internal Status values remain unchanged
- Add WriteImportReportHtml_ResultsSortedByStatus_SkippedFirst test verifying
sort order
- Update existing HTML tests: check for 'Success' instead of 'Imported',
verify dark theme CSS is present
(56c18cd) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(14d96e5) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(cda38fd) ~John KauffmanStore HTML import report in log directory with timestamp + add Open Report button
- Save HTML report only in the EQLP log directory (same dir as error log),
not in the NAG database directory, to avoid polluting user data
- Timestamped filename: eqlp-import-report-yyyy-MM-dd_HH-mm-ss.html
- Added GetLogDirectory() helper that reads the log4net FileAppender path
- Added 'Open Report' button to the NAG Import Complete dialog that opens
the HTML report in the default web browser
(3af0dc3) ~John KauffmanRemove CSV import report, keep HTML only
The CSV report (WriteImportReport + EscapeCsv) had no external consumers
and the HTML report is strictly more capable. Removed from NagUtil.cs
and TriggerUtil.cs; updated tests to use WriteImportReportHtml instead.
(8eaa9b6) ~John KauffmanUpdate NagUtil tests for comment format + add HTML report tests
- Update ConvertTriggers_Comments_PreservedInNagComment to assert that
'Original:' prefix is NOT present (removed in commit # [a63cce8](https://github.com/kauffman12/EQLogParser/commit/a63cce81))
- Update ConvertTriggers_DroppedFeatures_ListedInComment to check for
'EQLP Import Notes:' instead of 'Dropped:' (renamed in commit # [a63cce8](https://github.com/kauffman12/EQLogParser/commit/a63cce81))
- Add WriteImportReportHtml_ValidResults_CreatesHtml — verifies HTML
structure, summary stats, trigger names, folder paths, missing audio
files, and badge CSS classes
- Add WriteImportReportHtml_EmptyResults_CreatesHtml — verifies graceful
handling of empty results list
- Add WriteImportReportHtml_SpecialCharacters_AreHtmlEncoded — verifies
HTML encoding of < > & characters in trigger names
- Add WriteImportReportHtml_RootFolder_ShowsEmRoot — verifies root folder
path displays as <em>(root)</em>
(a39e8a5) ~John KauffmanAdd HTML import report alongside CSV
Adds WriteImportReportHtml() which generates a styled HTML report with
summary stats (imported/partial/skipped counts), color-coded status badges,
folder path display, and expandable missing audio file listings. The
report is generated as eqlp-import-report.html in the NAG database
directory, and both paths are shown in the import summary dialog.
(823d38c) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(85be2a4) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(d2c76fe) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(e885ed8) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(a63cce8) ~John KauffmanChanged import item
(72f9940) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(5f9f08f) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(f6f3678) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(868cff0) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(409f91b) ~John KauffmanMerge branch 'master' into develop
(46b8f52) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(3e7798f) ~John KauffmanUpdated custom audio selection
(b5e47f0) ~John KauffmanMerge branch 'develop' of https://github.com/kauffman12/EQLogParser into develop
(9b91446) ~John KauffmanCode cleanup
(eaf3e65) ~John KauffmanHandle custom audio files
(87ccb1d) ~John KauffmanImport nag v1
Bug Fixes
(8d56774) ~John KauffmanFix style issues and improved locking
(1ae4d79) ~John KauffmanFix trigger tester
(1cc590a) ~John KauffmanFix tests
(c3497dd) ~John KauffmanFix typo
(3da4357) ~John KauffmanFix resource leak and added tests
(d221dc0) ~John KauffmanFix for end clear variable not saving
(09c7846) ~John KauffmanFix description for end clear variables
(4e013d4) ~John KauffmanFix formatting
(7f01478) ~John KauffmanFix variable TTS related to timeout
(8f0cf23) ~John KauffmanFix variable expiration
(1ad5552) ~John KauffmanFix loading variable settings
(a59c0a0) ~John KauffmanFixed matches variable validator
(256bc20) ~John KauffmanFix for text value being cut short
(dc200b3) ~John KauffmanFix event handling and more UI updates
Performance
(1bc54e2) ~John KauffmanPerformance cleanup
(4f94b98) ~John KauffmanPerformance updates for variables
Styling
(8c04b58) ~John KauffmanStyle cleanup
Commits
(d2ecd7a) ~John Kauffman2.3.57
(f04f901) ~John KauffmanAdded trigger variables
(4560fe1) ~John KauffmanReview fixes
(feee4b3) ~John KauffmanUpdated dialog and docs
(a53d887) ~John KauffmanCleanup
(552de1f) ~John KauffmanBug fixes
(7785320) ~John KauffmanAdded progress dialog for import
(427692f) ~John KauffmanCleanup
(4a1a023) ~John KauffmanAdded variable clear when timer ends
(d191895) ~John KauffmanAdded command to clear variables
(b803939) ~John KauffmanChanged to Fixed
(82e7080) ~John KauffmanRenamed Value option
(3e24ce3) ~John KauffmanChanged internal types to number
(91190be) ~John KauffmanRenamed Text to Value
(47c0aa9) ~John KauffmanUpdated docs
(5cd4213) ~John KauffmanMade variable condition validation smarter
(22798ba) ~John KauffmanAdd validation for match variables field
(154d3d4) ~John KauffmanCleanup
(3503187) ~John KauffmanAdded trigger variables
(42c4cf8) ~John KauffmanUpdated tab names and fixes
(5954d52) ~John KauffmanAdded TTL to variables and updated style
(da52c33) ~John KauffmanUI cleanup and model updates
(65ca6c4) ~John KauffmanUI update for variables tab on trigger properties
Bug Fixes
(c219c16) ~John KauffmanFix tests
Commits
(0227f3c) ~John KauffmanV2.3.55
(fda2354) ~John KauffmanExpand all levels
(16e0d8c) ~John KauffmanHandle days when displaying timer
(ed30a2d) ~John KauffmanMerge branch 'master' into picode
(2f4391e) ~kauffman12PR # [392](https://github.com/kauffman12/EQLogParser/pull/392): text/timer overlay backgrounds rendering black on Wayland/Wine
(6936d5b) ~John KauffmanExtend {ts} labeled formats, add tree grid expand/collapse, and broaden trigger search
- Support labeled time formats in {ts} trigger tag (e.g. 4h:20m:53s, 1d:10h:20m:50s)
with optional components and flexible ordering. Update UpdateTimePattern regex
and refactor SimpleTimeToSeconds to detect and parse d/h/m/s suffixes.
- Add "Expand All" and "Collapse All" context menu items to all SfTreeGrid views
(Damage/Healing/Tanking Breakdown, Damage Summary, Taunt Stats, Random Viewer).
Commands are enabled only on SfTreeGrid targets with nodes.
- Extend trigger search to match against Comments, PreviousPattern,
EndEarlyPattern (1-3), and AltTimerName in addition to Name and Pattern.
(9051155) ~John KauffmanMerge branch 'master' into picode
(c4fa0b1) ~John KauffmanConsolidated safe file write
(8fec127) ~stvsuFix text/timer overlay backgrounds rendering black on Wayland/Wine
The text and timer overlays are AllowsTransparency windows that fill their
background and border with a fully-transparent brush (#00000000, alpha 0).
On wlroots-based Wayland compositors (e.g. Hyprland) running under Wine/Proton,
fully-transparent regions are not alpha-blended and render as opaque black,
while areas covered by drawn content (timer bars, text) composite correctly.
Windows and KDE/KWin are not affected.
Give the background and border a minimal non-zero alpha (#01000000, 1/255) so
the whole surface goes through the normal blend path. The change is visually
imperceptible and produces identical output on Windows and KDE.
(035bfa1) ~kauffman12PR # [389](https://github.com/kauffman12/EQLogParser/pull/389): Add support for 'cleave' 'reave' and 'smite' for DPS parser
- Add support for 'cleave' 'reave' and 'smite' for DPS parser
(bbcf45f) ~Cord B
(f9f6c51) ~John KauffmanRemoved unused interface
(a7122a4) ~John KauffmanUpdated install files list
(fc1751b) ~John KauffmanUpdate tests
(08dbdd8) ~John KauffmanPut iaction back
(1e0390f) ~John KauffmanUpdated tests
(4554849) ~John KauffmanLoad real spell data for tests
(5a2dca7) ~John KauffmanDamage and healing validator tests
(d108ea6) ~John KauffmanAdded tests and moved util classes to their own project
(1909420) ~John KauffmanChanged order or shutdown APIs
(08774fd) ~John KauffmanUpdated to use pattern matching