merge(vt): slice 1 Part A — VTank .usd/.utl drop-in and metaf .af for metas and routes (review-closed)
Campaign VT slice 1 Part A: the .usd document model + 137-setting serializer with declared type tags and exact compare, metaf .af reader/ writer for metas and nav routes with real byte identity against the owner's fixtures, .utl gate fixes, the VtankProfiles host storage (ACDREAM_VTANK_PROFILE_DIR), and the cutover of all four profile stores to real VTank files with one-time JSON migration. Two Opus lenses, three fix rounds, two narrow re-reviews, final re-check: MERGE-READY. Contract-doc ledger conflict resolved by keeping the campaign branch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
f680bf234a
108 changed files with 62970 additions and 1860 deletions
15
.gitattributes
vendored
15
.gitattributes
vendored
|
|
@ -1 +1,14 @@
|
||||||
.github/workflows/*.lock.yml linguist-generated=true merge=ours
|
.github/workflows/*.lock.yml linguist-generated=true merge=ours
|
||||||
|
|
||||||
|
# VTank file-compatibility fixtures (Campaign VT slice 1, Part A): these are
|
||||||
|
# real VTank .usd/.ast/.utl profiles and real metaf .af/.met/.nav samples
|
||||||
|
# whose exact CRLF-vs-LF byte layout is asserted by round-trip tests
|
||||||
|
# (VtankSettingsProfileSerializerTests, MetafSerializerTests). `-text`
|
||||||
|
# disables git's line-ending normalization entirely so a checkout on any OS
|
||||||
|
# or core.autocrlf setting reproduces the committed bytes exactly.
|
||||||
|
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/**/*.usd -text
|
||||||
|
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/**/*.ast -text
|
||||||
|
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/**/*.utl -text
|
||||||
|
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/**/*.af -text
|
||||||
|
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/**/*.met -text
|
||||||
|
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/**/*.nav -text
|
||||||
|
|
@ -162,6 +162,12 @@ namespace nor a machine-specific path. Hosts without durable storage expose
|
||||||
The additive `List(prefix)` operation enumerates only keys inside that same
|
The additive `List(prefix)` operation enumerates only keys inside that same
|
||||||
authenticated namespace, allowing plugins to discover explicit import/export
|
authenticated namespace, allowing plugins to discover explicit import/export
|
||||||
files without receiving a filesystem path or crossing plugin ownership.
|
files without receiving a filesystem path or crossing plugin ownership.
|
||||||
|
`IPluginHost.VtankProfiles` is a second, unscoped `IPluginStorage` — one
|
||||||
|
shared external location (a real installed VTank's own profile folder, or a
|
||||||
|
host-composed portable default) rather than per-plugin data — so a
|
||||||
|
VTank-compatible plugin (`AcDream.Plugins.MossTank.VtankProfileDirectory`)
|
||||||
|
can enumerate real `.usd`/`.ast`/`.af` files through the same BCL-only
|
||||||
|
contract without ever touching `System.IO` or resolving its own path.
|
||||||
|
|
||||||
`IPluginHost.Automation` is the additive gameplay-automation projection. Its
|
`IPluginHost.Automation` is the additive gameplay-automation projection. Its
|
||||||
character, spell, magic, chat, combat, equipment, item, loot, fellowship,
|
character, spell, magic, chat, combat, equipment, item, loot, fellowship,
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -238,6 +238,7 @@ $env:ACDREAM_FRAME_HISTORY = "$scratch\frames.csv"
|
||||||
| `ACDREAM_VULKAN_FORCE_UNSUPPORTED` | `=<feature-name>` (case-insensitive property name, e.g. `MultiDrawIndirect`) | Test knob (Slice V5): clears one named required Vulkan feature from the capability record to synthetically fail the gate, so the `NotSupportedException` → exit-code-4 → report path can be exercised on hardware that actually supports everything. | Deliberately breaks Vulkan startup when set to a matched feature name — this is a "make it fail on purpose" gate-testing flag, never appropriate for a normal or measurement run. | `null` → real capabilities used unmodified | `RuntimeOptions.VulkanForcedUnsupportedFeature` → `VulkanCapabilityRecord.Without` (`VulkanCapabilityRecord.cs:113-119`), consumed at `VulkanGraphicsContext.cs:339` |
|
| `ACDREAM_VULKAN_FORCE_UNSUPPORTED` | `=<feature-name>` (case-insensitive property name, e.g. `MultiDrawIndirect`) | Test knob (Slice V5): clears one named required Vulkan feature from the capability record to synthetically fail the gate, so the `NotSupportedException` → exit-code-4 → report path can be exercised on hardware that actually supports everything. | Deliberately breaks Vulkan startup when set to a matched feature name — this is a "make it fail on purpose" gate-testing flag, never appropriate for a normal or measurement run. | `null` → real capabilities used unmodified | `RuntimeOptions.VulkanForcedUnsupportedFeature` → `VulkanCapabilityRecord.Without` (`VulkanCapabilityRecord.cs:113-119`), consumed at `VulkanGraphicsContext.cs:339` |
|
||||||
| `ACDREAM_VULKAN_PROBE` | `=1` | Runs the standalone Vulkan capability-probe/bring-up harness (opens its own window, runs the capability gate, presents synthetic V6c/V6d verification scenes, captures one screenshot) **instead of** the real client composition host, then exits. | This flag ALONE gates entry (`GameWindow.cs:828`); the former `ACDREAM_RENDER_BACKEND=vulkan` co-requisite died with the OpenGL backend (its class doc was corrected 2026-08-24). | `false` → normal composition host | `RuntimeOptions.VulkanCapabilityProbe` → `GameWindow.cs:828` → `VulkanBringUpHost` |
|
| `ACDREAM_VULKAN_PROBE` | `=1` | Runs the standalone Vulkan capability-probe/bring-up harness (opens its own window, runs the capability gate, presents synthetic V6c/V6d verification scenes, captures one screenshot) **instead of** the real client composition host, then exits. | This flag ALONE gates entry (`GameWindow.cs:828`); the former `ACDREAM_RENDER_BACKEND=vulkan` co-requisite died with the OpenGL backend (its class doc was corrected 2026-08-24). | `false` → normal composition host | `RuntimeOptions.VulkanCapabilityProbe` → `GameWindow.cs:828` → `VulkanBringUpHost` |
|
||||||
| `ACDREAM_VULKAN_PROBE_FRAMES` | `=<int>` (non-negative) | Bounds the bring-up probe harness to N presented frames so it can run unattended in CI, instead of presenting until a human closes the window. | The frame budget never cuts a pending screenshot capture short — the loop stays open until the screenshot has been attempted even past the budget, so an unattended run's whole product (a PNG) is guaranteed. Zero (unset/unparseable/explicit `0`) keeps the interactive wait-for-close behavior. | `0` → interactive (wait for window close) | `RuntimeOptions.VulkanCapabilityProbeFrames` → `VulkanBringUpHost.cs:141-249` |
|
| `ACDREAM_VULKAN_PROBE_FRAMES` | `=<int>` (non-negative) | Bounds the bring-up probe harness to N presented frames so it can run unattended in CI, instead of presenting until a human closes the window. | The frame budget never cuts a pending screenshot capture short — the loop stays open until the screenshot has been attempted even past the budget, so an unattended run's whole product (a PNG) is guaranteed. Zero (unset/unparseable/explicit `0`) keeps the interactive wait-for-close behavior. | `0` → interactive (wait for window close) | `RuntimeOptions.VulkanCapabilityProbeFrames` → `VulkanBringUpHost.cs:141-249` |
|
||||||
|
| `ACDREAM_VTANK_PROFILE_DIR` | `=<path>` | Overrides the directory `IPluginHost.VtankProfiles` (a VTank-compatible plugin's real `.usd`/`.ast`/`.af` profile storage — see `AcDream.Plugins.MossTank.VtankProfileDirectory`) is rooted at, composed as a `FilePluginStorage`. Set it to a real installed VTank's own profile folder (e.g. `C:\Games\VirindiPlugins\VirindiTank`) for direct interop. | Redirects only that one plugin-storage root; no other startup behavior changes. An unset/empty-string value is treated as "no override" (`NullIfEmpty`); whitespace-only is NOT special-cased (matches every other `NullIfEmpty`-read flag, e.g. `ACDREAM_AC_DIR`/`ACDREAM_UI_PROBE_SCRIPT`) and would be used as a literal (almost certainly invalid) root. **Warning:** MossTank's own `.cdf` writes name its Nav/Meta profiles with the `.af` (metaf text) extension, not real VTank's native binary `.nav`/`.met` — pointed at a real installed VTank folder, that same character's Nav/Meta profiles become unloadable by the real VTank client sharing the directory (Settings `.usd` and Loot `.utl` stay real/binary-compatible; see register row AD-122). | unset → `<ApplicationPathSet.DataDirectory>/vtank` | `RuntimeOptions.VtankProfileDirectoryOverride` → `Program.cs` (composes `AppPluginHost`'s `vtankProfiles` argument). The Headless host has no equivalent override (its path overrides are `HeadlessPathOverrides`, not env vars) and always uses `HeadlessPathSet.VtankProfilesDirectory` (`<DataDirectory>/vtank`). |
|
||||||
| `ACDREAM_DUMP_MOVE_TRUTH` | `=1` | Emits one `move-truth OUT` line per outbound movement record (MoveToState / AutonomousPosition): local resolved position vs the wire position/cell, ground contact, velocity (`MovementTruthDiagnosticController`). | **Automation apparatus, NOT a spent probe** — the canonical nine-stop soak (`tools/run-connected-r6-soak.ps1`) hard-gates on ≥2 of these lines per destination as its proof that production input produced outbound movement traffic; deleting it fails the soak at every stop (#437, deleted-and-restored 2026-08-24). Print volume follows the outbound send cadence. | off | `RuntimeOptions.DumpMoveTruth` → `GameWindow.cs` → `MovementTruthDiagnosticController` |
|
| `ACDREAM_DUMP_MOVE_TRUTH` | `=1` | Emits one `move-truth OUT` line per outbound movement record (MoveToState / AutonomousPosition): local resolved position vs the wire position/cell, ground contact, velocity (`MovementTruthDiagnosticController`). | **Automation apparatus, NOT a spent probe** — the canonical nine-stop soak (`tools/run-connected-r6-soak.ps1`) hard-gates on ≥2 of these lines per destination as its proof that production input produced outbound movement traffic; deleting it fails the soak at every stop (#437, deleted-and-restored 2026-08-24). Print volume follows the outbound send cadence. | off | `RuntimeOptions.DumpMoveTruth` → `GameWindow.cs` → `MovementTruthDiagnosticController` |
|
||||||
|
|
||||||
## Permanent diagnostics
|
## Permanent diagnostics
|
||||||
|
|
|
||||||
|
|
@ -538,6 +538,23 @@ and `PetDeviceCatalog.cs` — but was not compared step-by-step against
|
||||||
`CombatController.cs`. Effect: minor DPS/time loss from unnecessary
|
`CombatController.cs`. Effect: minor DPS/time loss from unnecessary
|
||||||
re-wields when several adjacent monsters need different weapons,
|
re-wields when several adjacent monsters need different weapons,
|
||||||
lower impact than 1–3 above.
|
lower impact than 1–3 above.
|
||||||
|
6. **The real `.usd` `MyMonsters` table is preserved but never read or
|
||||||
|
written — a persistence gap, not a rule-grammar gap.** Round 3 item 11:
|
||||||
|
`CombatSettings.Rules` (the live `MonsterRule` list `MonsterRules.cs`/
|
||||||
|
`MonsterExpression.cs` evaluate against, item 5's own faithful port)
|
||||||
|
lives ONLY in MossTank's JSON side-car
|
||||||
|
(`MossTankProfileStore.SideCarDocument.CombatRules`); the real 21-column
|
||||||
|
`MyMonsters` table `VtankSettingsProfileSerializer` round-trips inside
|
||||||
|
the `.usd` file (`VtankSettingsProfileSerializer.cs:24-30`: "every table
|
||||||
|
other than Settings … is preserved byte-for-byte") is never parsed into
|
||||||
|
`MonsterRule`s on load and never regenerated from them on save. Effect:
|
||||||
|
a drop-in `.usd` from real VTank (or a hand-edited one) keeps its
|
||||||
|
`MyMonsters` rows completely inert in acdream — MossTank always uses
|
||||||
|
whatever the side-car separately holds instead, and a real VTank opening
|
||||||
|
an acdream-saved `.usd` would see stale/absent `MyMonsters` rows
|
||||||
|
regardless of what MossTank's own rule editor currently shows. Deferred
|
||||||
|
to a future slice (slice 3) that ports the table; NOT implemented this
|
||||||
|
round (see `docs/architecture/retail-divergence-register.md`).
|
||||||
|
|
||||||
`AutoAttackPower.cs` (melee power table) and `MonsterRules.cs` /
|
`AutoAttackPower.cs` (melee power table) and `MonsterRules.cs` /
|
||||||
`MonsterExpression.cs` (rule expression grammar) were both checked in
|
`MonsterExpression.cs` (rule expression grammar) were both checked in
|
||||||
|
|
|
||||||
|
|
@ -514,14 +514,20 @@ tests (`tests/AcDream.Plugins.MossTank.Tests/NavigationTests.cs`,
|
||||||
|
|
||||||
**Can a real `.nav` file load today? Yes.** `VtankNavRouteSerializer.TryLoad`
|
**Can a real `.nav` file load today? Yes.** `VtankNavRouteSerializer.TryLoad`
|
||||||
implements the exact header/route-type/waypoint-record grammar in §1,
|
implements the exact header/route-type/waypoint-record grammar in §1,
|
||||||
including the correct discard of the placeholder line, the correct
|
including the correct discard of the placeholder line, and the correct
|
||||||
per-type payload for all ten waypoint types, and — notably — correctly
|
per-type payload for all ten waypoint types. Portal2/UseNPC now keep both
|
||||||
special-cases Portal2/UseNPC to overwrite the (meaningless) outer header
|
coordinate triples on `RouteWaypoint` — the (meaningless, per §1.2) outer
|
||||||
coordinate with the embedded `d`-record's own coordinate (`VtankNavRouteSerializer.cs:151-157`).
|
header coordinate in `Position` and the embedded `d`-record's own real
|
||||||
It is wired to actual import via `MossTankRouteProfileStore.TryImportLegacy`,
|
target coordinate in `ReferencePosition` (`VtankNavRouteSerializer.cs:127-145`;
|
||||||
which reads a `.nav` text file from plugin storage by filename. Manual
|
Campaign VT slice-1 Part A fix round — a prior port overwrote `Position`
|
||||||
verification against `bunny_stuck_jump.nav` and `deathnav.nav` (§1.4) round-trips
|
with the `d`-record instead of keeping both, which made a `.af` round trip
|
||||||
cleanly against the documented grammar.
|
of the same waypoint lossy). `Navigation.TickUse` searches for the live
|
||||||
|
world object using `ReferencePosition`, matching retail's "real target
|
||||||
|
coordinate" role for that field. It is wired to actual import via
|
||||||
|
`MossTankRouteProfileStore.TryImportLegacy`, which reads a `.nav` text file
|
||||||
|
from plugin storage by filename. Manual verification against
|
||||||
|
`bunny_stuck_jump.nav` and `deathnav.nav` (§1.4) round-trips cleanly against
|
||||||
|
the documented grammar.
|
||||||
|
|
||||||
Ranked by impact (highest first):
|
Ranked by impact (highest first):
|
||||||
|
|
||||||
|
|
@ -535,6 +541,7 @@ Ranked by impact (highest first):
|
||||||
| 6 | **Portal2/UseNPC candidate filter omits the ground truth's `item.c()==0` gate.** | `e9.g()`/`fa.g()` only consider candidates where `item.c() == 0` — i.e. the object is neither contained nor equipped (`fu.c()` returns its Container or Wielder instance id, `fu.cs:66-87`) in addition to name+class+proximity. | `TryFindObject` is opaque from this file (defined elsewhere in the plugin abstraction); could not confirm whether an equivalent filter exists. | **Low** — flagged for follow-up rather than asserted as missing. |
|
| 6 | **Portal2/UseNPC candidate filter omits the ground truth's `item.c()==0` gate.** | `e9.g()`/`fa.g()` only consider candidates where `item.c() == 0` — i.e. the object is neither contained nor equipped (`fu.c()` returns its Container or Wielder instance id, `fu.cs:66-87`) in addition to name+class+proximity. | `TryFindObject` is opaque from this file (defined elsewhere in the plugin abstraction); could not confirm whether an equivalent filter exists. | **Low** — flagged for follow-up rather than asserted as missing. |
|
||||||
| 7 | **Chat-color gate on UseNPC's "got a response" detection is dropped.** | `fa.a(ChatTextInterceptEventArgs)` only accepts color-3 "tells you" or color-0 "gives you" lines (`fa.cs:160-177`). | `HasNpcResponse` (`Navigation.cs:829-848`) matches on text content and sender name only, with no color/channel check, plus an extra `Sender.Equals(npcName)` branch not present in ground truth. | **Low** — small false-positive risk (any channel's text matching the phrase would complete the node), unlikely to matter in practice given the fairly specific phrase match. |
|
| 7 | **Chat-color gate on UseNPC's "got a response" detection is dropped.** | `fa.a(ChatTextInterceptEventArgs)` only accepts color-3 "tells you" or color-0 "gives you" lines (`fa.cs:160-177`). | `HasNpcResponse` (`Navigation.cs:829-848`) matches on text content and sender name only, with no color/channel check, plus an extra `Sender.Equals(npcName)` branch not present in ground truth. | **Low** — small false-positive risk (any channel's text matching the phrase would complete the node), unlikely to matter in practice given the fairly specific phrase match. |
|
||||||
| 8 | **Door frame-count debounce vs. time-based retry.** | Portal2's post-use verification waits `PluginCore.@do >= startFrame + 2` — at least two *rendered frames*, not a duration — before checking arrival (`e9.cs:126-139`). | acdream's equivalents are all elapsed-seconds based (`UseRetrySeconds`, etc., `Navigation.cs:181` and throughout). | **Low** — a frame-based debounce doesn't map cleanly onto acdream's tick model in the first place; noted for completeness, not actionable. |
|
| 8 | **Door frame-count debounce vs. time-based retry.** | Portal2's post-use verification waits `PluginCore.@do >= startFrame + 2` — at least two *rendered frames*, not a duration — before checking arrival (`e9.cs:126-139`). | acdream's equivalents are all elapsed-seconds based (`UseRetrySeconds`, etc., `Navigation.cs:181` and throughout). | **Low** — a frame-based debounce doesn't map cleanly onto acdream's tick model in the first place; noted for completeness, not actionable. |
|
||||||
|
| 9 | **`.af` (metaf) cannot represent a strafe jump's direction at all.** | The binary `.nav` format's Jump record ends with one combined line encoding both charge-ms and a direction digit ∈ {3=Forward,4=StrafeLeft,5=StrafeRight} (§1.2 row 9, `di.cs:116-172`). | metaf's `NJump` class (`metaf_monolithic.py:11708-11821`, both `ImportFromMetAF`/`ExportToMetAF`) has no direction field whatsoever — only x/y/z, heading, holdShift, and delay-ms. `RouteWaypoint.JumpDirection` survives a `.nav`⇄model round trip exactly, but a route saved to `.af` and reloaded always comes back `Forward` regardless of what it held before the save, because the `.af` text itself never carried the value. | **Medium** — a real, unavoidable format limitation (not a porting gap): confirmed by reading metaf's own class end to end, not inferred. `MetafSerializer`'s `.af` writer does not claim otherwise and the reader does not force-assign `Forward` (it leaves the model's own default), but the value is still lost across a save-as-`.af`/reload cycle for StrafeLeft/StrafeRight waypoints. |
|
||||||
|
|
||||||
Correctly and precisely ported (confirmed, not a gap — listed since they
|
Correctly and precisely ported (confirmed, not a gap — listed since they
|
||||||
were non-obvious and worth recording as verified rather than re-litigated):
|
were non-obvious and worth recording as verified rather than re-litigated):
|
||||||
|
|
|
||||||
|
|
@ -727,6 +727,7 @@ line counts). This is an unusually precise reverse-engineering result — the
|
||||||
| 3 | **`MonsterNameCountWithinDistance`'s name pattern is case-insensitive in MossTank, case-sensitive in retail** | `d7.c()` compiles with `RegexOptions.Compiled` only (`d7.cs:26`) — case-sensitive | `Meta.cs`'s `MonsterCount` helper (`Meta.cs:536-552`) compiles with `RegexOptions.IgnoreCase \| RegexOptions.CultureInvariant` | **Medium-high.** A pattern authored against retail's case-sensitive matching (e.g. deliberately excluding a differently-cased variant name) will over-match in MossTank. Note `ChatMessage`/`ChatMessageCapture` do **not** have this divergence — MossTank's `ChatMatch` (`Meta.cs:463-513`) is correctly case-sensitive (`RegexOptions.CultureInvariant` only), matching `hl.cs`/`c5.cs`. |
|
| 3 | **`MonsterNameCountWithinDistance`'s name pattern is case-insensitive in MossTank, case-sensitive in retail** | `d7.c()` compiles with `RegexOptions.Compiled` only (`d7.cs:26`) — case-sensitive | `Meta.cs`'s `MonsterCount` helper (`Meta.cs:536-552`) compiles with `RegexOptions.IgnoreCase \| RegexOptions.CultureInvariant` | **Medium-high.** A pattern authored against retail's case-sensitive matching (e.g. deliberately excluding a differently-cased variant name) will over-match in MossTank. Note `ChatMessage`/`ChatMessageCapture` do **not** have this divergence — MossTank's `ChatMatch` (`Meta.cs:463-513`) is correctly case-sensitive (`RegexOptions.CultureInvariant` only), matching `hl.cs`/`c5.cs`. |
|
||||||
| 4 | **`;` sequence-operator value convention is inverted vs. retail (but matches UtilityBelt, and was chosen deliberately)** | Retail VTank: `a;b` evaluates to **`a`** (the left/first operand), discarding `b`'s value, while still executing `b` for side effects (`ExpressionEvaluator.cs:787-790`); `;` is an ordinary operator that can nest anywhere via normal precedence | `ExpressionProgram.Evaluate` (`ExpressionEngine.cs:20-27`) treats top-level `;`-separated statements as a `Node[]` program and returns the value of the **last** statement — this matches UtilityBelt's own audited grammar ("multiple `;`-separated statements, returning the final result", `2026-08-26-mosstank-vtank-utilitybelt-research.md:237`), which the campaign explicitly chose as MossTank's baseline dialect (§3.1 there) | **Medium, and by design, not an oversight.** Retail VTank and UtilityBelt already disagree with each other on `;`'s return value; MossTank correctly implements UtilityBelt's convention. The compat risk is narrower than a plain bug: only a `.met` authored *against retail's own* semantics (chaining `sideeffect[]; realcheck[]` and relying on the *first* value) evaluates to the opposite result once imported. |
|
| 4 | **`;` sequence-operator value convention is inverted vs. retail (but matches UtilityBelt, and was chosen deliberately)** | Retail VTank: `a;b` evaluates to **`a`** (the left/first operand), discarding `b`'s value, while still executing `b` for side effects (`ExpressionEvaluator.cs:787-790`); `;` is an ordinary operator that can nest anywhere via normal precedence | `ExpressionProgram.Evaluate` (`ExpressionEngine.cs:20-27`) treats top-level `;`-separated statements as a `Node[]` program and returns the value of the **last** statement — this matches UtilityBelt's own audited grammar ("multiple `;`-separated statements, returning the final result", `2026-08-26-mosstank-vtank-utilitybelt-research.md:237`), which the campaign explicitly chose as MossTank's baseline dialect (§3.1 there) | **Medium, and by design, not an oversight.** Retail VTank and UtilityBelt already disagree with each other on `;`'s return value; MossTank correctly implements UtilityBelt's convention. The compat risk is narrower than a plain bug: only a `.met` authored *against retail's own* semantics (chaining `sideeffect[]; realcheck[]` and relying on the *first* value) evaluates to the opposite result once imported. |
|
||||||
| 5 | **Watchdog ring pre-fill differs (far sentinel vs. arm-time position)** | All 10 position-history slots start as a sentinel far from any real coordinate (`(1000,1000,1000)`, `h7.cs:45-49`), guaranteeing expiry cannot even be *reachable* until one full `timeSpan` window has elapsed and every slot has been overwritten with a real sample | `SetWatchdog` (`Meta.cs:585-596`) pre-fills all 10 slots with the **current** position at arm time | **Lower-medium.** Both converge on "earliest possible expiry is ~one `TimeSpanSeconds` after arming" in the common case, but they diverge for a watchdog that is armed, then the player leaves and returns to very near the arm point before the window closes — retail's sentinel-seeded ring cannot spuriously read the arm-time position as one of its 10 samples, MossTank's can. |
|
| 5 | **Watchdog ring pre-fill differs (far sentinel vs. arm-time position)** | All 10 position-history slots start as a sentinel far from any real coordinate (`(1000,1000,1000)`, `h7.cs:45-49`), guaranteeing expiry cannot even be *reachable* until one full `timeSpan` window has elapsed and every slot has been overwritten with a real sample | `SetWatchdog` (`Meta.cs:585-596`) pre-fills all 10 slots with the **current** position at arm time | **Lower-medium.** Both converge on "earliest possible expiry is ~one `TimeSpanSeconds` after arming" in the common case, but they diverge for a watchdog that is armed, then the player leaves and returns to very near the arm point before the window closes — retail's sentinel-seeded ring cannot spuriously read the arm-time position as one of its 10 samples, MossTank's can. |
|
||||||
|
| 6 | **`MetaRule.Enabled` (a per-rule disable-without-delete toggle) has no metaf-compatible representation at all** | Real VTank/metaf has no such concept — `metaf_monolithic.py` has zero occurrences of "enabled"/"disabled" anywhere; a rule in a `.met`/`.af` is simply present or absent | `MetafSerializer.SaveMeta` (Campaign VT slice-1 item H) **refuses to save** a profile containing any disabled rule by default (throws `InvalidOperationException` naming the count) rather than silently dropping it — the caller must opt in via `SaveMeta(profile, dropDisabledRules: true)` to accept the loss explicitly. Round 2 (Campaign VT slice 1 Part A) made `.af` the SOLE authoritative Meta store — `MossTankMetaProfileStore` has no separate JSON storage or `.af` "convenience mirror" any more, so this refusal now blocks the save of the profile itself: a disabled rule makes that profile file genuinely **unsaveable** until the user re-enables or deletes the rule (the file on disk keeps its last successfully-saved content in the meantime; round 3 item 12 corrected this row, which previously described the pre-cutover "legacy-export mirror" design). | **Low** (by design, not a bug): this is a MossTank-only UI extension with no VTank equivalent to diverge from, and the refusal is loud (a user-visible notice) rather than a silent data loss. |
|
||||||
|
|
||||||
### 5.3 Confirmed non-gaps (the format/engine is otherwise unusually faithful)
|
### 5.3 Confirmed non-gaps (the format/engine is otherwise unusually faithful)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ public sealed class AppPluginHost : IPluginHost
|
||||||
IAutomationSurface automation,
|
IAutomationSurface automation,
|
||||||
IPluginStorage? storage = null,
|
IPluginStorage? storage = null,
|
||||||
IPluginCommandRegistry? commands = null,
|
IPluginCommandRegistry? commands = null,
|
||||||
IPluginLootClassifierRegistry? lootClassifiers = null)
|
IPluginLootClassifierRegistry? lootClassifiers = null,
|
||||||
|
IPluginStorage? vtankProfiles = null)
|
||||||
{
|
{
|
||||||
Log = log;
|
Log = log;
|
||||||
State = state;
|
State = state;
|
||||||
|
|
@ -25,6 +26,7 @@ public sealed class AppPluginHost : IPluginHost
|
||||||
Commands = commands ?? NoOpPluginCommandRegistry.Instance;
|
Commands = commands ?? NoOpPluginCommandRegistry.Instance;
|
||||||
LootClassifiers = lootClassifiers
|
LootClassifiers = lootClassifiers
|
||||||
?? NoOpPluginLootClassifierRegistry.Instance;
|
?? NoOpPluginLootClassifierRegistry.Instance;
|
||||||
|
VtankProfiles = vtankProfiles ?? NoOpPluginStorage.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HasUi => true;
|
public bool HasUi => true;
|
||||||
|
|
@ -37,4 +39,5 @@ public sealed class AppPluginHost : IPluginHost
|
||||||
public IPluginStorage Storage { get; }
|
public IPluginStorage Storage { get; }
|
||||||
public IPluginCommandRegistry Commands { get; }
|
public IPluginCommandRegistry Commands { get; }
|
||||||
public IPluginLootClassifierRegistry LootClassifiers { get; }
|
public IPluginLootClassifierRegistry LootClassifiers { get; }
|
||||||
|
public IPluginStorage VtankProfiles { get; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,12 @@ internal sealed class FilePluginStorage : IPluginStorage
|
||||||
|
|
||||||
public IReadOnlyList<string> List(string prefix)
|
public IReadOnlyList<string> List(string prefix)
|
||||||
{
|
{
|
||||||
string directory = Resolve(prefix);
|
ArgumentNullException.ThrowIfNull(prefix);
|
||||||
|
// An empty prefix means "the storage root itself" — Resolve()
|
||||||
|
// rejects an empty/whitespace key (every other caller of it means
|
||||||
|
// one specific file or sub-directory), so this is handled directly
|
||||||
|
// rather than relaxing that guard for every other use.
|
||||||
|
string directory = prefix.Length == 0 ? _root : Resolve(prefix);
|
||||||
if (!Directory.Exists(directory))
|
if (!Directory.Exists(directory))
|
||||||
return Array.Empty<string>();
|
return Array.Empty<string>();
|
||||||
return Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories)
|
return Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories)
|
||||||
|
|
|
||||||
16
src/AcDream.App/Plugins/VtankProfilesDefault.cs
Normal file
16
src/AcDream.App/Plugins/VtankProfilesDefault.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
namespace AcDream.App.Plugins;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The graphical host's default root for <c>IPluginHost.VtankProfiles</c>
|
||||||
|
/// when <c>RuntimeOptions.VtankProfileDirectoryOverride</c>
|
||||||
|
/// (<c>ACDREAM_VTANK_PROFILE_DIR</c>) is unset — extracted out of
|
||||||
|
/// <c>Program.cs</c>'s inline composition into its own pure, injectable-root
|
||||||
|
/// function so the "built with <see cref="Path.Combine(string, string)"/>
|
||||||
|
/// only, never a hard-coded Windows path" guarantee is a real, failable unit
|
||||||
|
/// test rather than something only checkable by reading the source.
|
||||||
|
/// </summary>
|
||||||
|
internal static class VtankProfilesDefault
|
||||||
|
{
|
||||||
|
internal static string Resolve(string dataDirectory) =>
|
||||||
|
Path.Combine(dataDirectory, "vtank");
|
||||||
|
}
|
||||||
|
|
@ -185,7 +185,10 @@ var host = new AppPluginHost(
|
||||||
new FilePluginStorage(
|
new FilePluginStorage(
|
||||||
Path.Combine(applicationPaths.ConfigDirectory, "plugins")),
|
Path.Combine(applicationPaths.ConfigDirectory, "plugins")),
|
||||||
automation.PluginCommands,
|
automation.PluginCommands,
|
||||||
lootClassifiers);
|
lootClassifiers,
|
||||||
|
new FilePluginStorage(
|
||||||
|
runtimeOptions.VtankProfileDirectoryOverride
|
||||||
|
?? VtankProfilesDefault.Resolve(applicationPaths.DataDirectory)));
|
||||||
GraphicalPluginSession pluginSession = GraphicalPluginSession.Create(
|
GraphicalPluginSession pluginSession = GraphicalPluginSession.Create(
|
||||||
applicationPaths,
|
applicationPaths,
|
||||||
runtimeOptions.Plugins,
|
runtimeOptions.Plugins,
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,17 @@ public sealed record RuntimeOptions(
|
||||||
/// process configuration directly.</summary>
|
/// process configuration directly.</summary>
|
||||||
public IReadOnlyList<string> PluginTags { get; init; } = [];
|
public IReadOnlyList<string> PluginTags { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>ACDREAM_VTANK_PROFILE_DIR</c> override for the directory a
|
||||||
|
/// VTank-compatible plugin's <c>IPluginHost.VtankProfiles</c> storage is
|
||||||
|
/// rooted at (real <c>.usd</c>/<c>.ast</c>/<c>.af</c> files — e.g. a real
|
||||||
|
/// installed VTank's own profile folder for direct interop).
|
||||||
|
/// <see langword="null"/> (the default) means "no opinion": the host
|
||||||
|
/// composes <c>applicationPaths.DataDirectory/vtank</c> instead. See
|
||||||
|
/// <c>docs/launch-options.md</c>.
|
||||||
|
/// </summary>
|
||||||
|
public string? VtankProfileDirectoryOverride { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Build options from the process environment. Used by
|
/// Build options from the process environment. Used by
|
||||||
/// <c>Program.cs</c> at startup.
|
/// <c>Program.cs</c> at startup.
|
||||||
|
|
@ -271,6 +282,7 @@ public sealed record RuntimeOptions(
|
||||||
LoginCommandDelayMs: 500)
|
LoginCommandDelayMs: 500)
|
||||||
{
|
{
|
||||||
PluginTags = ParsePluginTags(env("ACDREAM_PLUGIN_TAGS")),
|
PluginTags = ParsePluginTags(env("ACDREAM_PLUGIN_TAGS")),
|
||||||
|
VtankProfileDirectoryOverride = NullIfEmpty(env("ACDREAM_VTANK_PROFILE_DIR")),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,14 @@ internal sealed class ScopedPluginHost : IPluginHost, IDisposable
|
||||||
public ISelectionService Selection => _selection;
|
public ISelectionService Selection => _selection;
|
||||||
public IUiRegistry Ui => _ui;
|
public IUiRegistry Ui => _ui;
|
||||||
public IPluginStorage Storage => _storage;
|
public IPluginStorage Storage => _storage;
|
||||||
|
/// <summary>
|
||||||
|
/// Forwarded, not scoped, unlike <see cref="Storage"/>: the VTank
|
||||||
|
/// profile folder is one shared external location (real VTank's own
|
||||||
|
/// files, or a host-composed portable default), not per-plugin data —
|
||||||
|
/// scoping it under this plugin's manifest id would defeat the whole
|
||||||
|
/// point of pointing it at a real installed VTank profile directory.
|
||||||
|
/// </summary>
|
||||||
|
public IPluginStorage VtankProfiles => _inner.VtankProfiles;
|
||||||
public IPluginCommandRegistry Commands => _commands;
|
public IPluginCommandRegistry Commands => _commands;
|
||||||
public IPluginLootClassifierRegistry LootClassifiers => _lootClassifiers;
|
public IPluginLootClassifierRegistry LootClassifiers => _lootClassifiers;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,8 @@ internal sealed class HeadlessProcessHost : IDisposable
|
||||||
Path.Combine(AppContext.BaseDirectory, "plugins"),
|
Path.Combine(AppContext.BaseDirectory, "plugins"),
|
||||||
paths.PluginsDirectory,
|
paths.PluginsDirectory,
|
||||||
];
|
];
|
||||||
|
var vtankProfiles = new AcDream.Headless.Plugins.FilePluginStorage(
|
||||||
|
paths.VtankProfilesDirectory);
|
||||||
HeadlessProcessContentOwner? content = null;
|
HeadlessProcessContentOwner? content = null;
|
||||||
HeadlessProcessResourceSampler? resources = null;
|
HeadlessProcessResourceSampler? resources = null;
|
||||||
// FA6: constructed unconditionally — cheap, and every non-gate
|
// FA6: constructed unconditionally — cheap, and every non-gate
|
||||||
|
|
@ -110,7 +112,8 @@ internal sealed class HeadlessProcessHost : IDisposable
|
||||||
timeProvider,
|
timeProvider,
|
||||||
contentLease: contentLease,
|
contentLease: contentLease,
|
||||||
gateCoordinator: gateCoordinator,
|
gateCoordinator: gateCoordinator,
|
||||||
pluginRoots: pluginRoots));
|
pluginRoots: pluginRoots,
|
||||||
|
vtankProfiles: vtankProfiles));
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using AcDream.Headless.Credentials;
|
||||||
using AcDream.Headless.Diagnostics;
|
using AcDream.Headless.Diagnostics;
|
||||||
using AcDream.Headless.Plugins;
|
using AcDream.Headless.Plugins;
|
||||||
using AcDream.Headless.Policies;
|
using AcDream.Headless.Policies;
|
||||||
|
using AcDream.Plugin.Abstractions;
|
||||||
using AcDream.Content.CharGen;
|
using AcDream.Content.CharGen;
|
||||||
using AcDream.Core.Chat;
|
using AcDream.Core.Chat;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
|
|
@ -266,7 +267,8 @@ internal sealed class HeadlessSessionHost : IDisposable
|
||||||
IHeadlessBotPolicy? policyOverride = null,
|
IHeadlessBotPolicy? policyOverride = null,
|
||||||
IRuntimePlacementProjectionSink? placementSinkOverride = null,
|
IRuntimePlacementProjectionSink? placementSinkOverride = null,
|
||||||
FellowshipAllegianceGateCoordinator? gateCoordinator = null,
|
FellowshipAllegianceGateCoordinator? gateCoordinator = null,
|
||||||
IEnumerable<string>? pluginRoots = null)
|
IEnumerable<string>? pluginRoots = null,
|
||||||
|
IPluginStorage? vtankProfiles = null)
|
||||||
{
|
{
|
||||||
_descriptor = descriptor
|
_descriptor = descriptor
|
||||||
?? throw new ArgumentNullException(nameof(descriptor));
|
?? throw new ArgumentNullException(nameof(descriptor));
|
||||||
|
|
@ -367,7 +369,8 @@ internal sealed class HeadlessSessionHost : IDisposable
|
||||||
descriptor.Id,
|
descriptor.Id,
|
||||||
pluginRoots ?? [],
|
pluginRoots ?? [],
|
||||||
descriptor.Plugins,
|
descriptor.Plugins,
|
||||||
pluginCommands);
|
pluginCommands,
|
||||||
|
vtankProfiles);
|
||||||
var liveSession = new LiveSessionHost(
|
var liveSession = new LiveSessionHost(
|
||||||
runtime.Session,
|
runtime.Session,
|
||||||
new LiveSessionHostBindings(
|
new LiveSessionHostBindings(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,17 @@ internal sealed record HeadlessPathSet(
|
||||||
internal string PluginsDirectory =>
|
internal string PluginsDirectory =>
|
||||||
Path.Combine(DataDirectory, "plugins");
|
Path.Combine(DataDirectory, "plugins");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default root for <see cref="AcDream.Plugin.Abstractions.IPluginHost.VtankProfiles"/>
|
||||||
|
/// (Campaign VT slice-1 fix round, item F). No override mechanism of its
|
||||||
|
/// own yet — unlike the graphical host's <c>ACDREAM_VTANK_PROFILE_DIR</c>,
|
||||||
|
/// which only exists on <c>AcDream.App.RuntimeOptions</c> — because
|
||||||
|
/// headless path overrides already go through <c>HeadlessPathOverrides</c>
|
||||||
|
/// (config file / <c>--data-dir</c>), not environment variables.
|
||||||
|
/// </summary>
|
||||||
|
internal string VtankProfilesDirectory =>
|
||||||
|
Path.Combine(DataDirectory, "vtank");
|
||||||
|
|
||||||
internal static HeadlessPathSet Resolve(
|
internal static HeadlessPathSet Resolve(
|
||||||
HeadlessPathOverrides overrides,
|
HeadlessPathOverrides overrides,
|
||||||
IHeadlessPlatformEnvironment? platform = null)
|
IHeadlessPlatformEnvironment? platform = null)
|
||||||
|
|
|
||||||
99
src/AcDream.Headless/Plugins/FilePluginStorage.cs
Normal file
99
src/AcDream.Headless/Plugins/FilePluginStorage.cs
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
using System.Text;
|
||||||
|
using AcDream.Plugin.Abstractions;
|
||||||
|
|
||||||
|
namespace AcDream.Headless.Plugins;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crash-safe filesystem <see cref="IPluginStorage"/> — byte-identical
|
||||||
|
/// contract to <c>AcDream.App.Plugins.FilePluginStorage</c>. Duplicated
|
||||||
|
/// rather than shared: <c>AcDream.Headless</c> does not (and per the
|
||||||
|
/// no-window/graphical layer split should not) reference <c>AcDream.App</c>,
|
||||||
|
/// and no shared "platform plugins" library exists yet to host one copy of
|
||||||
|
/// this ~70-line class for both hosts. Promoting it there is a reasonable
|
||||||
|
/// future cleanup, not required for Campaign VT slice-1 item F.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class FilePluginStorage : IPluginStorage
|
||||||
|
{
|
||||||
|
private readonly string _root;
|
||||||
|
|
||||||
|
internal FilePluginStorage(string root)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(root);
|
||||||
|
_root = Path.GetFullPath(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsAvailable => true;
|
||||||
|
|
||||||
|
public string? ReadText(string key)
|
||||||
|
{
|
||||||
|
string path = Resolve(key);
|
||||||
|
return File.Exists(path)
|
||||||
|
? File.ReadAllText(path, Encoding.UTF8)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<string> List(string prefix)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(prefix);
|
||||||
|
// An empty prefix means "the storage root itself" — Resolve()
|
||||||
|
// rejects an empty/whitespace key (every other caller of it means
|
||||||
|
// one specific file or sub-directory), so this is handled directly
|
||||||
|
// rather than relaxing that guard for every other use.
|
||||||
|
string directory = prefix.Length == 0 ? _root : Resolve(prefix);
|
||||||
|
if (!Directory.Exists(directory))
|
||||||
|
return Array.Empty<string>();
|
||||||
|
return Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories)
|
||||||
|
.Select(path => Path.GetRelativePath(_root, path)
|
||||||
|
.Replace(Path.DirectorySeparatorChar, '/'))
|
||||||
|
.OrderBy(static key => key, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteText(string key, string content)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(content);
|
||||||
|
string path = Resolve(key);
|
||||||
|
string directory = Path.GetDirectoryName(path)!;
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
string temporary = Path.Combine(
|
||||||
|
directory,
|
||||||
|
$".{Path.GetFileName(path)}.{Guid.NewGuid():N}.tmp");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(temporary, content, new UTF8Encoding(false));
|
||||||
|
File.Move(temporary, path, overwrite: true);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (File.Exists(temporary))
|
||||||
|
File.Delete(temporary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Delete(string key)
|
||||||
|
{
|
||||||
|
string path = Resolve(key);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
return false;
|
||||||
|
File.Delete(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Resolve(string key)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(key);
|
||||||
|
if (Path.IsPathRooted(key))
|
||||||
|
throw new ArgumentException("Plugin storage keys must be relative.", nameof(key));
|
||||||
|
string path = Path.GetFullPath(Path.Combine(_root, key));
|
||||||
|
string relative = Path.GetRelativePath(_root, path);
|
||||||
|
if (Path.IsPathRooted(relative)
|
||||||
|
|| relative.Equals("..", StringComparison.Ordinal)
|
||||||
|
|| relative.StartsWith(
|
||||||
|
".." + Path.DirectorySeparatorChar,
|
||||||
|
StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Plugin storage key escapes its root.", nameof(key));
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -39,17 +39,20 @@ internal sealed class HeadlessPluginHost
|
||||||
internal HeadlessPluginHost(
|
internal HeadlessPluginHost(
|
||||||
GameRuntime runtime,
|
GameRuntime runtime,
|
||||||
IPluginLogger logger,
|
IPluginLogger logger,
|
||||||
IPluginCommandRegistry? commands = null)
|
IPluginCommandRegistry? commands = null,
|
||||||
|
IPluginStorage? vtankProfiles = null)
|
||||||
{
|
{
|
||||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||||
Log = logger ?? throw new ArgumentNullException(nameof(logger));
|
Log = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
Commands = commands ?? NoOpPluginCommandRegistry.Instance;
|
Commands = commands ?? NoOpPluginCommandRegistry.Instance;
|
||||||
|
VtankProfiles = vtankProfiles ?? NoOpPluginStorage.Instance;
|
||||||
_eventSubscription = runtime.Subscribe(this);
|
_eventSubscription = runtime.Subscribe(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HasUi => false;
|
public bool HasUi => false;
|
||||||
public IPluginLogger Log { get; }
|
public IPluginLogger Log { get; }
|
||||||
public IPluginCommandRegistry Commands { get; }
|
public IPluginCommandRegistry Commands { get; }
|
||||||
|
public IPluginStorage VtankProfiles { get; }
|
||||||
public IGameState State => this;
|
public IGameState State => this;
|
||||||
public IEvents Events => this;
|
public IEvents Events => this;
|
||||||
public ISelectionService Selection => _runtime.ActionOwner.Selection;
|
public ISelectionService Selection => _runtime.ActionOwner.Selection;
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,8 @@ internal sealed class HeadlessPluginSession : IDisposable
|
||||||
string sessionId,
|
string sessionId,
|
||||||
IEnumerable<string> roots,
|
IEnumerable<string> roots,
|
||||||
IReadOnlyList<string>? allowList,
|
IReadOnlyList<string>? allowList,
|
||||||
IPluginCommandRegistry? commands = null)
|
IPluginCommandRegistry? commands = null,
|
||||||
|
IPluginStorage? vtankProfiles = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(runtime);
|
ArgumentNullException.ThrowIfNull(runtime);
|
||||||
ArgumentNullException.ThrowIfNull(diagnostics);
|
ArgumentNullException.ThrowIfNull(diagnostics);
|
||||||
|
|
@ -59,7 +60,8 @@ internal sealed class HeadlessPluginSession : IDisposable
|
||||||
diagnostics,
|
diagnostics,
|
||||||
sessionId,
|
sessionId,
|
||||||
() => runtime.Generation.Value),
|
() => runtime.Generation.Value),
|
||||||
commands);
|
commands,
|
||||||
|
vtankProfiles);
|
||||||
var plugins = new PluginSession(
|
var plugins = new PluginSession(
|
||||||
host,
|
host,
|
||||||
status => Report(statusWriter, sessionId, status),
|
status => Report(statusWriter, sessionId, status),
|
||||||
|
|
|
||||||
|
|
@ -42,4 +42,22 @@ public interface IPluginHost
|
||||||
/// host kind.
|
/// host kind.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IAutomationSurface Automation { get; }
|
IAutomationSurface Automation { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Storage rooted at the VTank profile folder (real <c>.usd</c>/
|
||||||
|
/// <c>.ast</c>/<c>.af</c> files, VTank's own naming rules) rather than
|
||||||
|
/// this plugin's own scoped <see cref="Storage"/> directory — see
|
||||||
|
/// <c>AcDream.Plugins.MossTank.VtankProfileDirectory</c>, which
|
||||||
|
/// enumerates through this property exclusively (no <c>System.IO</c>,
|
||||||
|
/// no per-OS portable-default fallback of its own) so directory
|
||||||
|
/// discovery stays entirely host-composed. Defaults to the inert
|
||||||
|
/// <see cref="NoOpPluginStorage"/> (<c>IsAvailable</c> false) when the
|
||||||
|
/// host has no opinion. A graphical host may root this at a real
|
||||||
|
/// installed VTank's own profile directory for direct interop, or at
|
||||||
|
/// its own portable per-OS default under
|
||||||
|
/// <c>ApplicationPathSet.DataDirectory</c>; that discovery belongs
|
||||||
|
/// entirely to the host composing this property, never to the plugin
|
||||||
|
/// reading it.
|
||||||
|
/// </summary>
|
||||||
|
IPluginStorage VtankProfiles => NoOpPluginStorage.Instance;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,5 +24,6 @@
|
||||||
</None>
|
</None>
|
||||||
<EmbeddedResource Include="VtankCraftRecipes.tsv" />
|
<EmbeddedResource Include="VtankCraftRecipes.tsv" />
|
||||||
<EmbeddedResource Include="VtankAmmunitionOptions.tsv" />
|
<EmbeddedResource Include="VtankAmmunitionOptions.tsv" />
|
||||||
|
<EmbeddedResource Include="VtankDefaultSettings.usd" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -174,11 +174,11 @@ internal sealed class AttackSpellCatalog
|
||||||
{
|
{
|
||||||
if (shape == AttackSpellShape.Streak)
|
if (shape == AttackSpellShape.Streak)
|
||||||
return 1;
|
return 1;
|
||||||
if (settings.UseArcs && target.Distance >= settings.ArcRange)
|
if (ShouldUseArc(settings, target))
|
||||||
return shape == AttackSpellShape.Arc ? 2 : 3;
|
return shape == AttackSpellShape.Arc ? 2 : 3;
|
||||||
return shape == AttackSpellShape.Direct ? 2 : 3;
|
return shape == AttackSpellShape.Direct ? 2 : 3;
|
||||||
}
|
}
|
||||||
if (settings.UseArcs && target.Distance >= settings.ArcRange)
|
if (ShouldUseArc(settings, target))
|
||||||
{
|
{
|
||||||
if (shape == AttackSpellShape.Arc)
|
if (shape == AttackSpellShape.Arc)
|
||||||
return 1;
|
return 1;
|
||||||
|
|
@ -203,6 +203,19 @@ internal sealed class AttackSpellCatalog
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VTank's real 3-way UseArcs switch (<c>refs/vtank/decompiled/hi.cs:515-538</c>):
|
||||||
|
/// <c>No</c> never arcs, <c>Yes</c> always arcs, <c>AtRange</c> arcs only
|
||||||
|
/// once the target is at or beyond <see cref="CombatSettings.ArcRange"/>.
|
||||||
|
/// </summary>
|
||||||
|
private static bool ShouldUseArc(CombatSettings settings, PluginCombatTarget target) =>
|
||||||
|
settings.UseArcs switch
|
||||||
|
{
|
||||||
|
UseArcsMode.Yes => true,
|
||||||
|
UseArcsMode.AtRange => target.Distance >= settings.ArcRange,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
private static bool MatchesPrimaryShape(
|
private static bool MatchesPrimaryShape(
|
||||||
AttackSpellShape shape,
|
AttackSpellShape shape,
|
||||||
MonsterRuleActions actions,
|
MonsterRuleActions actions,
|
||||||
|
|
|
||||||
|
|
@ -243,11 +243,11 @@ internal sealed class CombatController
|
||||||
_untilScan -= Math.Max(0d, elapsedSeconds);
|
_untilScan -= Math.Max(0d, elapsedSeconds);
|
||||||
if (_untilScan <= 0d)
|
if (_untilScan <= 0d)
|
||||||
{
|
{
|
||||||
float acquisitionRange = navigationEnabled
|
double acquisitionRange = navigationEnabled
|
||||||
? Math.Max(_settings.MaximumRange, _settings.ApproachDistance)
|
? Math.Max(_settings.MaximumRange, _settings.ApproachDistance)
|
||||||
: _settings.MaximumRange;
|
: _settings.MaximumRange;
|
||||||
_targets = _host.Automation.Combat.CaptureHostileTargets(
|
_targets = _host.Automation.Combat.CaptureHostileTargets(
|
||||||
acquisitionRange);
|
(float)acquisitionRange);
|
||||||
foreach (uint ghost in _failures.ObserveTargets(
|
foreach (uint ghost in _failures.ObserveTargets(
|
||||||
_targets,
|
_targets,
|
||||||
_now,
|
_now,
|
||||||
|
|
@ -588,7 +588,7 @@ internal sealed class CombatController
|
||||||
|
|
||||||
float maximumRange = spell.BaseRangeConstant
|
float maximumRange = spell.BaseRangeConstant
|
||||||
+ (spell.BaseRangeModifier * skill.Current)
|
+ (spell.BaseRangeModifier * skill.Current)
|
||||||
- _settings.SpellRangeFudge;
|
- (float)_settings.SpellRangeFudge;
|
||||||
return maximumRange <= 0f
|
return maximumRange <= 0f
|
||||||
|| target.ObjectId == 0u
|
|| target.ObjectId == 0u
|
||||||
|| target.Distance <= MathF.Min(75f, maximumRange);
|
|| target.Distance <= MathF.Min(75f, maximumRange);
|
||||||
|
|
@ -1087,15 +1087,15 @@ internal sealed class CombatController
|
||||||
targetObjectId,
|
targetObjectId,
|
||||||
kind,
|
kind,
|
||||||
height,
|
height,
|
||||||
_settings.CollisionProjectileRadius,
|
(float)_settings.CollisionProjectileRadius,
|
||||||
_settings.CollisionStepDistance,
|
(float)_settings.CollisionStepDistance,
|
||||||
_settings.MaximumCollisionChecksPerTick)
|
_settings.MaximumCollisionChecksPerTick)
|
||||||
: _host.Automation.Projectiles.EvaluatePath(
|
: _host.Automation.Projectiles.EvaluatePath(
|
||||||
targetObjectId,
|
targetObjectId,
|
||||||
kind,
|
kind,
|
||||||
height,
|
height,
|
||||||
_settings.CollisionProjectileRadius,
|
(float)_settings.CollisionProjectileRadius,
|
||||||
_settings.CollisionStepDistance,
|
(float)_settings.CollisionStepDistance,
|
||||||
_settings.MaximumCollisionChecksPerTick);
|
_settings.MaximumCollisionChecksPerTick);
|
||||||
if (_settings.ShowCollisionDebug && result.DebugSamples.Count > 0)
|
if (_settings.ShowCollisionDebug && result.DebugSamples.Count > 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,23 @@ internal enum DebuffSelectionMethod
|
||||||
Skill = 2,
|
Skill = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VTank's real 3-way "UseArcs" enum
|
||||||
|
/// (<c>refs/vtank/decompiled/hi.cs:515-538</c>, switch on
|
||||||
|
/// <c>f3.f("UseArcs")</c>): <c>No</c> always picks the direct-shape spell,
|
||||||
|
/// <c>AtRange</c> picks the arc shape only once the target is at or beyond
|
||||||
|
/// <c>ArcRange</c>, and <c>Yes</c> always picks the arc shape regardless of
|
||||||
|
/// distance. A prior port collapsed this onto a single bool (effectively
|
||||||
|
/// only distinguishing "No" from "AtRange"), which cannot represent "Yes"
|
||||||
|
/// at all — see <see cref="AttackSpellCatalog"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal enum UseArcsMode
|
||||||
|
{
|
||||||
|
No = 1,
|
||||||
|
AtRange = 2,
|
||||||
|
Yes = 3,
|
||||||
|
}
|
||||||
|
|
||||||
internal enum PetRangeMode
|
internal enum PetRangeMode
|
||||||
{
|
{
|
||||||
AttackDistance = 0,
|
AttackDistance = 0,
|
||||||
|
|
@ -52,25 +69,30 @@ internal sealed class CombatSettings
|
||||||
public bool Enabled { get; set; } = true;
|
public bool Enabled { get; set; } = true;
|
||||||
/// <summary>VTank's hunt-cast skill margin.</summary>
|
/// <summary>VTank's hunt-cast skill margin.</summary>
|
||||||
public int HuntSkillExcessOverDifficulty { get; set; } = 25;
|
public int HuntSkillExcessOverDifficulty { get; set; } = 25;
|
||||||
public float MaximumRange { get; set; } = 5f;
|
// Declared tDouble in VTank's own Settings table (VtankOptionCatalog):
|
||||||
|
// double, not float, so a loaded .usd round-trips this setting exactly
|
||||||
|
// instead of losing precision below float's ~7-digit guarantee (item I,
|
||||||
|
// slice-1 fix round). Physics/combat consumers that want a float cast
|
||||||
|
// at their own use site.
|
||||||
|
public double MaximumRange { get; set; } = 5d;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Monsters nearer than this are not valid attack targets. VTank applies
|
/// Monsters nearer than this are not valid attack targets. VTank applies
|
||||||
/// this before priority and angle/range ranking.
|
/// this before priority and angle/range ranking.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float MinimumRange { get; set; }
|
public double MinimumRange { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// VTank's Approach Distance. Zero disables monster approach; otherwise
|
/// VTank's Approach Distance. Zero disables monster approach; otherwise
|
||||||
/// navigation may close a selected target from this range down to
|
/// navigation may close a selected target from this range down to
|
||||||
/// <see cref="MaximumRange"/>.
|
/// <see cref="MaximumRange"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float ApproachDistance { get; set; }
|
public double ApproachDistance { get; set; }
|
||||||
public bool IdlePeaceMode { get; set; }
|
public bool IdlePeaceMode { get; set; }
|
||||||
public bool StopMacroOnDeath { get; set; } = true;
|
public bool StopMacroOnDeath { get; set; } = true;
|
||||||
public bool JumpOutWandCasting { get; set; }
|
public bool JumpOutWandCasting { get; set; }
|
||||||
public bool DoJiggle { get; set; }
|
public bool DoJiggle { get; set; }
|
||||||
public TargetSelectionMethod SelectionMethod { get; set; } =
|
public TargetSelectionMethod SelectionMethod { get; set; } =
|
||||||
TargetSelectionMethod.Both;
|
TargetSelectionMethod.Both;
|
||||||
public float TargetSelectAngleRange { get; set; } = 5f;
|
public double TargetSelectAngleRange { get; set; } = 5d;
|
||||||
public bool TargetLock { get; set; }
|
public bool TargetLock { get; set; }
|
||||||
public PluginAttackHeight AttackHeight { get; set; } =
|
public PluginAttackHeight AttackHeight { get; set; } =
|
||||||
PluginAttackHeight.Medium;
|
PluginAttackHeight.Medium;
|
||||||
|
|
@ -83,16 +105,16 @@ internal sealed class CombatSettings
|
||||||
DebuffSelectionMethod.Skill;
|
DebuffSelectionMethod.Skill;
|
||||||
public double DebuffPrecastSeconds { get; set; } = 5d;
|
public double DebuffPrecastSeconds { get; set; } = 5d;
|
||||||
public bool SwitchWandsToDebuff { get; set; }
|
public bool SwitchWandsToDebuff { get; set; }
|
||||||
public bool UseArcs { get; set; } = true;
|
public UseArcsMode UseArcs { get; set; } = UseArcsMode.AtRange;
|
||||||
public float SpellRangeFudge { get; set; } = 1f;
|
public double SpellRangeFudge { get; set; } = 1d;
|
||||||
public bool UseBreakableTurnTo { get; set; } = true;
|
public bool UseBreakableTurnTo { get; set; } = true;
|
||||||
public bool UseProjectileAwareness { get; set; } = true;
|
public bool UseProjectileAwareness { get; set; } = true;
|
||||||
public float CollisionProjectileRadius { get; set; } = 0.4f;
|
public double CollisionProjectileRadius { get; set; } = 0.4d;
|
||||||
public float CollisionStepDistance { get; set; } = 0.7f;
|
public double CollisionStepDistance { get; set; } = 0.7d;
|
||||||
public bool ShowCollisionDebug { get; set; }
|
public bool ShowCollisionDebug { get; set; }
|
||||||
public int MaximumCollisionChecksPerTick { get; set; } = 500;
|
public int MaximumCollisionChecksPerTick { get; set; } = 500;
|
||||||
public float ArcRange { get; set; } = 5f;
|
public double ArcRange { get; set; } = 5d;
|
||||||
public float RingDistance { get; set; } = 5f;
|
public double RingDistance { get; set; } = 5d;
|
||||||
public int MinimumRingTargets { get; set; } = 4;
|
public int MinimumRingTargets { get; set; } = 4;
|
||||||
public bool DeleteGhostMonsters { get; set; } = true;
|
public bool DeleteGhostMonsters { get; set; } = true;
|
||||||
public int GhostMonsterSpellAttemptCount { get; set; } = 200;
|
public int GhostMonsterSpellAttemptCount { get; set; } = 200;
|
||||||
|
|
@ -102,7 +124,7 @@ internal sealed class CombatSettings
|
||||||
public double GhostDeleteHealthTrackerSeconds { get; set; } = 30d;
|
public double GhostDeleteHealthTrackerSeconds { get; set; } = 30d;
|
||||||
public bool SummonPets { get; set; } = true;
|
public bool SummonPets { get; set; } = true;
|
||||||
public PetRangeMode PetRangeMode { get; set; } = PetRangeMode.AttackDistance;
|
public PetRangeMode PetRangeMode { get; set; } = PetRangeMode.AttackDistance;
|
||||||
public float PetCustomRange { get; set; } = 5f;
|
public double PetCustomRange { get; set; } = 5d;
|
||||||
public int PetMonsterDensity { get; set; } = 1;
|
public int PetMonsterDensity { get; set; } = 1;
|
||||||
public int PetRefillCountIdle { get; set; } = 3;
|
public int PetRefillCountIdle { get; set; } = 3;
|
||||||
public int PetRefillCountNormal { get; set; } = 1;
|
public int PetRefillCountNormal { get; set; } = 1;
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,10 @@ internal sealed class LootSettings
|
||||||
public bool CombineSalvage { get; set; } = true;
|
public bool CombineSalvage { get; set; } = true;
|
||||||
public int ManaStoneLootCount { get; set; } = 4;
|
public int ManaStoneLootCount { get; set; } = 4;
|
||||||
public int ManaTankMinimumMana { get; set; } = 1000;
|
public int ManaTankMinimumMana { get; set; } = 1000;
|
||||||
public float CorpseApproachRange { get; set; } = 40f;
|
// Declared tDouble in VTank's own Settings table (item I, slice-1 fix
|
||||||
public float CorpseMinimumApproachRange { get; set; } = 3.36f;
|
// round) — see CombatSettings.MaximumRange's doc comment for why.
|
||||||
|
public double CorpseApproachRange { get; set; } = 40d;
|
||||||
|
public double CorpseMinimumApproachRange { get; set; } = 3.36d;
|
||||||
public double CorpseOpenTimeoutSeconds { get; set; } = 1.5d;
|
public double CorpseOpenTimeoutSeconds { get; set; } = 1.5d;
|
||||||
public double CorpseItemAppearanceTimeoutSeconds { get; set; } = 6d;
|
public double CorpseItemAppearanceTimeoutSeconds { get; set; } = 6d;
|
||||||
public double CorpseItemIdentifyTimeoutSeconds { get; set; } = 60d;
|
public double CorpseItemIdentifyTimeoutSeconds { get; set; } = 60d;
|
||||||
|
|
@ -524,7 +526,7 @@ internal sealed class LootController
|
||||||
_scanRemaining = Math.Clamp(_settings.ScanIntervalSeconds, 0.05d, 5d);
|
_scanRemaining = Math.Clamp(_settings.ScanIntervalSeconds, 0.05d, 5d);
|
||||||
|
|
||||||
IReadOnlyList<PluginLootContainer> corpses = loot.CaptureCorpses(
|
IReadOnlyList<PluginLootContainer> corpses = loot.CaptureCorpses(
|
||||||
Math.Clamp(_settings.CorpseApproachRange, 2f, 100f));
|
(float)Math.Clamp(_settings.CorpseApproachRange, 2d, 100d));
|
||||||
PruneCorpseCache();
|
PruneCorpseCache();
|
||||||
foreach (PluginLootContainer seen in corpses)
|
foreach (PluginLootContainer seen in corpses)
|
||||||
_corpseFirstSeen.TryAdd(seen.ObjectId, _lifetime);
|
_corpseFirstSeen.TryAdd(seen.ObjectId, _lifetime);
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,17 @@ internal sealed class MetaAction
|
||||||
public double Number { get; set; }
|
public double Number { get; set; }
|
||||||
public double SecondaryNumber { get; set; }
|
public double SecondaryNumber { get; set; }
|
||||||
public List<MetaAction> Children { get; set; } = [];
|
public List<MetaAction> Children { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// The parsed route for a <see cref="MetaActionKind.LoadEmbeddedNavigationRoute"/>
|
||||||
|
/// action. Replaces a prior port's binary "uTank2 NAV 1.2" text blob
|
||||||
|
/// carried in <see cref="Text"/>: both the <c>.af</c> importer
|
||||||
|
/// (<c>MetafSerializer.ResolveEmbeddedNavs</c>) and the <c>.met</c>
|
||||||
|
/// importer (<c>VtankMetaProfileSerializer.ReadEmbeddedNavigation</c>)
|
||||||
|
/// now produce this typed model directly, and
|
||||||
|
/// <c>MetaEngine.LoadEmbeddedNavigationRoute</c> consumes it without a
|
||||||
|
/// re-parse. Null only for an unresolved/never-defined tag.
|
||||||
|
/// </summary>
|
||||||
|
public NavigationSettings? EmbeddedRoute { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class MetaRule
|
internal sealed class MetaRule
|
||||||
|
|
@ -102,7 +113,7 @@ internal sealed class MetaServices
|
||||||
static () => double.PositiveInfinity;
|
static () => double.PositiveInfinity;
|
||||||
public Func<int, double, int> CountMonstersByPriority { get; init; } =
|
public Func<int, double, int> CountMonstersByPriority { get; init; } =
|
||||||
static (_, _) => 0;
|
static (_, _) => 0;
|
||||||
public Action<string> LoadEmbeddedNavigationRoute { get; init; } = static _ => { };
|
public Action<NavigationSettings?> LoadEmbeddedNavigationRoute { get; init; } = static _ => { };
|
||||||
public Func<string, ExpressionValue> GetOption { get; init; } =
|
public Func<string, ExpressionValue> GetOption { get; init; } =
|
||||||
static _ => ExpressionValue.Zero;
|
static _ => ExpressionValue.Zero;
|
||||||
public Func<string, ExpressionValue, bool> SetOption { get; init; } =
|
public Func<string, ExpressionValue, bool> SetOption { get; init; } =
|
||||||
|
|
@ -379,7 +390,7 @@ internal sealed class MetaEngine
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
case MetaActionKind.LoadEmbeddedNavigationRoute:
|
case MetaActionKind.LoadEmbeddedNavigationRoute:
|
||||||
_services.LoadEmbeddedNavigationRoute(action.Text);
|
_services.LoadEmbeddedNavigationRoute(action.EmbeddedRoute);
|
||||||
return true;
|
return true;
|
||||||
case MetaActionKind.CallMetaState:
|
case MetaActionKind.CallMetaState:
|
||||||
if (_callStack.Count >= MaximumCallDepth)
|
if (_callStack.Count >= MaximumCallDepth)
|
||||||
|
|
|
||||||
1332
src/AcDream.Plugins.MossTank/MetafSerializer.cs
Normal file
1332
src/AcDream.Plugins.MossTank/MetafSerializer.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -241,10 +241,7 @@ internal sealed partial class MossTankPanel
|
||||||
if (_profiles.Create(
|
if (_profiles.Create(
|
||||||
name,
|
name,
|
||||||
copyCurrent: true,
|
copyCurrent: true,
|
||||||
_combatSettings,
|
_allSettings,
|
||||||
_buffSettings,
|
|
||||||
_vitalSettings,
|
|
||||||
_inventorySettings,
|
|
||||||
_noBuffItemNames,
|
_noBuffItemNames,
|
||||||
out string notice))
|
out string notice))
|
||||||
{
|
{
|
||||||
|
|
@ -271,7 +268,17 @@ internal sealed partial class MossTankPanel
|
||||||
WriteVtank("Usage: /vt nav [save/load] [filename]");
|
WriteVtank("Usage: /vt nav [save/load] [filename]");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
name = StripExtension(name, ".nav");
|
// Item J (slice-1 fix round): .af is the only VTank-compatible
|
||||||
|
// storage format now (Campaign VT slice 1 Part A); ".nav" is
|
||||||
|
// stripped for legacy-typed names, ".af" for names copy-pasted
|
||||||
|
// from a real .af file (the VtankProfiles directory, or an
|
||||||
|
// imports/ drop-in for TryImportLegacy below) — without both,
|
||||||
|
// "/vt nav save Foo.af" would have produced a doubled "Foo.af.af"
|
||||||
|
// file. Round 3 item 12: this comment previously cited an
|
||||||
|
// "exports/nav/" mirror directory that no longer exists — route
|
||||||
|
// profiles have written directly to their real .af file since the
|
||||||
|
// round 2 step 2-3 cutover, with no separate export copy.
|
||||||
|
name = StripExtension(name, ".nav", ".af");
|
||||||
if (operation == "save")
|
if (operation == "save")
|
||||||
{
|
{
|
||||||
_routeProfiles.Create(
|
_routeProfiles.Create(
|
||||||
|
|
@ -353,7 +360,9 @@ internal sealed partial class MossTankPanel
|
||||||
WriteVtank("Usage: /vt meta [save/load] [filename]");
|
WriteVtank("Usage: /vt meta [save/load] [filename]");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
name = StripExtension(name, ".met", ".json");
|
// Item J (slice-1 fix round): .af is the only VTank-compatible
|
||||||
|
// storage format now — see the analogous nav-command comment above.
|
||||||
|
name = StripExtension(name, ".met", ".json", ".af");
|
||||||
if (operation == "save")
|
if (operation == "save")
|
||||||
{
|
{
|
||||||
_metaProfiles.Create(
|
_metaProfiles.Create(
|
||||||
|
|
@ -420,19 +429,28 @@ internal sealed partial class MossTankPanel
|
||||||
WriteVtank("Option set: Invalid option specified.");
|
WriteVtank("Option set: Invalid option specified.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (rawValue.Length == 0 || !TryParseOptionValue(rawValue, out ExpressionValue value))
|
canonical = VtankOptionCatalog.Canonical(name);
|
||||||
|
VtankSettingValueType declaredType = VtankOptionCatalog.DeclaredType(canonical);
|
||||||
|
if (rawValue.Length == 0 || !TryParseOptionValue(rawValue, declaredType, out ExpressionValue value))
|
||||||
{
|
{
|
||||||
WriteVtank("Option set: Invalid value specified.");
|
// Round 3 item 6: retail's exact failure text
|
||||||
|
// (refs/vtank/decompiled/uTank2/PluginCore.cs:5501,5508,5612)
|
||||||
|
// names the option and the CLR type its Settings row
|
||||||
|
// actually declares (gy's own m_a, ToString()'d).
|
||||||
|
WriteVtank(
|
||||||
|
"Option set: Invalid value specified. Proper type of "
|
||||||
|
+ canonical + " is " + DeclaredClrTypeName(declaredType) + ".");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
canonical = VtankOptionCatalog.Canonical(name);
|
|
||||||
SetMetaOption(canonical, value);
|
SetMetaOption(canonical, value);
|
||||||
if (operation.Equals("setinall", StringComparison.OrdinalIgnoreCase))
|
if (operation.Equals("setinall", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
int count = _profiles.SetOptionInAll(
|
// Round 3 item 6: retail's exact bk.a text
|
||||||
canonical,
|
// (refs/vtank/decompiled/bk.cs:32) — no "= value" suffix,
|
||||||
ToMonsterValue(value));
|
// and the count is every .usd file scanned, not just the
|
||||||
WriteVtank($"Set option {canonical} in {count} profile(s) = {GetMetaOption(canonical).ToDisplayString()}");
|
// ones that already had a row for this name.
|
||||||
|
int count = _profiles.SetOptionInAll(canonical, _allSettings);
|
||||||
|
WriteVtank($"Done saving setting {canonical} to all profiles. (Changed {count} profiles)");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -754,9 +772,9 @@ internal sealed partial class MossTankPanel
|
||||||
private void TestPet()
|
private void TestPet()
|
||||||
{
|
{
|
||||||
IReadOnlyList<PluginCombatTarget> targets = _host.Automation.Combat
|
IReadOnlyList<PluginCombatTarget> targets = _host.Automation.Combat
|
||||||
.CaptureHostileTargets(_combatSettings.PetRangeMode == PetRangeMode.Custom
|
.CaptureHostileTargets((float)(_combatSettings.PetRangeMode == PetRangeMode.Custom
|
||||||
? _combatSettings.PetCustomRange
|
? _combatSettings.PetCustomRange
|
||||||
: _combatSettings.MaximumRange);
|
: _combatSettings.MaximumRange));
|
||||||
PetAutomationChoice choice = PetAutomation.Select(
|
PetAutomationChoice choice = PetAutomation.Select(
|
||||||
_host.Automation.Items.CaptureOwnedItems(),
|
_host.Automation.Items.CaptureOwnedItems(),
|
||||||
targets,
|
targets,
|
||||||
|
|
@ -962,22 +980,107 @@ internal sealed partial class MossTankPanel
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryParseOptionValue(string source, out ExpressionValue value)
|
/// <summary>
|
||||||
|
/// Free-form parse (no catalog type to check against) for callers like
|
||||||
|
/// the Advanced Options editor, which can target names outside the 137-
|
||||||
|
/// row catalog: best-effort bool, then number, then string.
|
||||||
|
/// </summary>
|
||||||
|
private static bool TryParseOptionValue(string source, out ExpressionValue value) =>
|
||||||
|
TryParseOptionValue(source, declaredType: null, out value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round 3 item 6: <c>/vt opt set</c>/<c>setinall</c> validate the typed
|
||||||
|
/// value against the catalog's OWN declared <see cref="VtankSettingValueType"/>
|
||||||
|
/// (retail's <c>eSettingValueType</c>) rather than accepting anything —
|
||||||
|
/// a value that does not parse as that type fails with retail's exact
|
||||||
|
/// "Proper type of X is Y." text (<see cref="HandleOptionCommand"/>).
|
||||||
|
/// <paramref name="declaredType"/> is <see langword="null"/> only for
|
||||||
|
/// the free-form Advanced Options editor, which keeps the original lax
|
||||||
|
/// best-effort behavior.
|
||||||
|
/// </summary>
|
||||||
|
private static bool TryParseOptionValue(
|
||||||
|
string source,
|
||||||
|
VtankSettingValueType? declaredType,
|
||||||
|
out ExpressionValue value)
|
||||||
{
|
{
|
||||||
if (bool.TryParse(source, out bool boolean))
|
if (source.Length == 0)
|
||||||
{
|
{
|
||||||
value = ExpressionValue.Boolean(boolean);
|
value = default;
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
if (double.TryParse(source, NumberStyles.Float, CultureInfo.InvariantCulture, out double number))
|
switch (declaredType)
|
||||||
{
|
{
|
||||||
value = ExpressionValue.Number(number);
|
case VtankSettingValueType.Bool:
|
||||||
return true;
|
if (bool.TryParse(source, out bool boolean))
|
||||||
|
{
|
||||||
|
value = ExpressionValue.Boolean(boolean);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
value = default;
|
||||||
|
return false;
|
||||||
|
case VtankSettingValueType.Int:
|
||||||
|
case VtankSettingValueType.Enum:
|
||||||
|
if (int.TryParse(
|
||||||
|
source, NumberStyles.Integer, CultureInfo.InvariantCulture, out int integer))
|
||||||
|
{
|
||||||
|
value = ExpressionValue.Number(integer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
value = default;
|
||||||
|
return false;
|
||||||
|
case VtankSettingValueType.Double:
|
||||||
|
case VtankSettingValueType.Single:
|
||||||
|
if (double.TryParse(
|
||||||
|
source, NumberStyles.Float, CultureInfo.InvariantCulture, out double number))
|
||||||
|
{
|
||||||
|
value = ExpressionValue.Number(number);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
value = default;
|
||||||
|
return false;
|
||||||
|
case VtankSettingValueType.String:
|
||||||
|
value = ExpressionValue.String(source);
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
// No declared type (free-form editor) or VtankSettingValueType.Custom
|
||||||
|
// (the one non-scalar row, RechargeHandlerSet, never reached
|
||||||
|
// through /vt opt set): keep the original lax parse.
|
||||||
|
if (bool.TryParse(source, out bool freeBoolean))
|
||||||
|
{
|
||||||
|
value = ExpressionValue.Boolean(freeBoolean);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (double.TryParse(
|
||||||
|
source, NumberStyles.Float, CultureInfo.InvariantCulture, out double freeNumber))
|
||||||
|
{
|
||||||
|
value = ExpressionValue.Number(freeNumber);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
value = ExpressionValue.String(source);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
value = ExpressionValue.String(source);
|
|
||||||
return source.Length != 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The CLR type name retail's exact failure text names
|
||||||
|
/// (<c>refs/vtank/decompiled/uTank2/PluginCore.cs:5501,5508,5612</c>:
|
||||||
|
/// <c>cw2[1].b().ToString()</c> — the Settings row's underlying
|
||||||
|
/// <c>gy</c> cell type, <see cref="Type.ToString"/>'d). Retail stores an
|
||||||
|
/// Enum-declared row as a plain int cell (confirmed against the
|
||||||
|
/// UseArcs row in the shipped defaultsettings.usd fixture), so it also
|
||||||
|
/// reads "System.Int32".
|
||||||
|
/// </summary>
|
||||||
|
private static string DeclaredClrTypeName(VtankSettingValueType type) => type switch
|
||||||
|
{
|
||||||
|
VtankSettingValueType.Bool => "System.Boolean",
|
||||||
|
VtankSettingValueType.Double => "System.Double",
|
||||||
|
VtankSettingValueType.Int => "System.Int32",
|
||||||
|
VtankSettingValueType.Single => "System.Single",
|
||||||
|
VtankSettingValueType.String => "System.String",
|
||||||
|
VtankSettingValueType.Enum => "System.Int32",
|
||||||
|
_ => "System.Object",
|
||||||
|
};
|
||||||
|
|
||||||
private bool TryParseCoordinates(string source, out PluginNavigationPosition position)
|
private bool TryParseCoordinates(string source, out PluginNavigationPosition position)
|
||||||
{
|
{
|
||||||
position = default;
|
position = default;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Globalization;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
@ -6,13 +7,32 @@ using AcDream.Plugin.Abstractions;
|
||||||
namespace AcDream.Plugins.MossTank;
|
namespace AcDream.Plugins.MossTank;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Independent VTank loot-profile lifecycle. Macro settings select this
|
/// VTank-compatible loot profile lifecycle. <c>.utl</c> (VTClassic's real
|
||||||
/// profile by character, but its ordered rules live in their own document.
|
/// public format, <see cref="VtankLootProfileSerializer"/>) is the ONLY
|
||||||
|
/// storage/authoring format — round 3 item 9 cutover, matching the
|
||||||
|
/// Settings/.usd, Route/.af, and Meta/.af cutovers already landed. Real
|
||||||
|
/// VTank's own loot picker (<c>aa()</c>, <c>uTank2/PluginCore.cs:7127-7154</c>)
|
||||||
|
/// seeds ONLY <c>[None]</c> — there is no per-character auto loot file and
|
||||||
|
/// no "mine only" filter for loot at all
|
||||||
|
/// (<c>docs/research/vtank-kb/01-settings-and-profiles.md</c> section 3).
|
||||||
|
/// MossTank keeps its own established <see cref="ByCharacter"/> convention
|
||||||
|
/// (already load-bearing for the other three stores) as a recorded
|
||||||
|
/// adaptation rather than a retail behavior.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class MossTankLootProfileStore
|
internal sealed class MossTankLootProfileStore
|
||||||
{
|
{
|
||||||
public const string ByCharacter = "By char";
|
public const string ByCharacter = "By char";
|
||||||
private const string IndexKey = "profiles/loot/index.json";
|
|
||||||
|
// The pre-cutover roster (round 3 item 9): before this cutover, this
|
||||||
|
// key was the LIVE index (Names + SelectedByCharacter) — unlike Meta/
|
||||||
|
// Route's already-abandoned pre-round-2 rosters, loot's roster was
|
||||||
|
// still actively read/written until this exact commit. Read-only now,
|
||||||
|
// consulted ONLY by the one-time SweepLegacyRosterIfNeeded (which also
|
||||||
|
// converts the SELECTED profile, so there is no separate
|
||||||
|
// MigrateLegacyIfNeeded here the way Settings/Meta/Route each have —
|
||||||
|
// the sweep already covers every named profile, selected or not).
|
||||||
|
private const string LegacyRosterKey = "profiles/loot/index.json";
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions Options = new()
|
private static readonly JsonSerializerOptions Options = new()
|
||||||
{
|
{
|
||||||
WriteIndented = true,
|
WriteIndented = true,
|
||||||
|
|
@ -20,62 +40,89 @@ internal sealed class MossTankLootProfileStore
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly IPluginHost _host;
|
private readonly IPluginHost _host;
|
||||||
private IndexDocument _index;
|
|
||||||
private string _characterName = string.Empty;
|
private string _characterName = string.Empty;
|
||||||
private string _selected = ByCharacter;
|
private string _selected = ByCharacter;
|
||||||
|
private bool _rosterSwept;
|
||||||
|
|
||||||
public MossTankLootProfileStore(IPluginHost host)
|
public MossTankLootProfileStore(IPluginHost host)
|
||||||
{
|
{
|
||||||
_host = host ?? throw new ArgumentNullException(nameof(host));
|
_host = host ?? throw new ArgumentNullException(nameof(host));
|
||||||
_index = Read<IndexDocument>(IndexKey) ?? new IndexDocument();
|
|
||||||
_index.Names ??= [];
|
|
||||||
_index.SelectedByCharacter = new Dictionary<string, string>(
|
|
||||||
_index.SelectedByCharacter ?? new Dictionary<string, string>(),
|
|
||||||
StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Selected => _selected;
|
/// <summary>
|
||||||
|
/// The bare name a user typed to select/create this profile (the file
|
||||||
|
/// name minus its <c>.utl</c> extension) — internal identity
|
||||||
|
/// (<see cref="_selected"/>) always carries the real, on-disk file
|
||||||
|
/// name; only display strips it, matching
|
||||||
|
/// <see cref="MossTankMetaProfileStore.Selected"/>'s own convention.
|
||||||
|
/// </summary>
|
||||||
|
public string Selected => StripUtl(_selected);
|
||||||
public string? RecoveryNotice { get; private set; }
|
public string? RecoveryNotice { get; private set; }
|
||||||
public IReadOnlyList<string> AvailableNames => new[] { ByCharacter }
|
|
||||||
.Concat(_index.Names)
|
private static string StripUtl(string name) => name.Equals(
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
ByCharacter, StringComparison.OrdinalIgnoreCase)
|
||||||
.OrderBy(name => name.Equals(
|
? name
|
||||||
ByCharacter,
|
: name.EndsWith(".utl", StringComparison.OrdinalIgnoreCase)
|
||||||
StringComparison.OrdinalIgnoreCase) ? 0 : 1)
|
? name[..^4]
|
||||||
.ThenBy(static name => name, StringComparer.OrdinalIgnoreCase)
|
: name;
|
||||||
.ToArray();
|
|
||||||
|
private string Server => _host.Automation.Character.WorldName;
|
||||||
|
private IPluginStorage VtankStorage => _host.VtankProfiles;
|
||||||
|
private bool CanBindFiles => _characterName.Length > 0 && Server.Length > 0;
|
||||||
|
|
||||||
|
public IReadOnlyList<string> AvailableNames
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var names = new List<string> { ByCharacter };
|
||||||
|
foreach (VtankProfileDirectory.ProfileEntry entry in
|
||||||
|
VtankProfileDirectory.ListLootProfiles(VtankStorage))
|
||||||
|
{
|
||||||
|
if (entry.FileName.Length == 0)
|
||||||
|
continue; // VTank's own "[None]"/MossTank's "[By char]" sentinels.
|
||||||
|
names.Add(StripUtl(entry.FileName));
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool BindCharacter(string? characterName)
|
public bool BindCharacter(string? characterName)
|
||||||
{
|
{
|
||||||
string normalized = string.IsNullOrWhiteSpace(characterName)
|
string normalized = string.IsNullOrWhiteSpace(characterName)
|
||||||
? string.Empty
|
? string.Empty
|
||||||
: characterName.Trim();
|
: characterName.Trim();
|
||||||
if (string.Equals(
|
if (string.Equals(normalized, _characterName, StringComparison.OrdinalIgnoreCase))
|
||||||
normalized,
|
|
||||||
_characterName,
|
|
||||||
StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
_characterName = normalized;
|
_characterName = normalized;
|
||||||
_selected = _index.SelectedByCharacter.TryGetValue(
|
VtankProfileDirectory.VtankCharacterBinding? binding = CanBindFiles
|
||||||
SelectionKey(),
|
? VtankProfileDirectory.TryReadCharacterBinding(VtankStorage, _characterName, Server)
|
||||||
out string? selected)
|
: null;
|
||||||
&& IsKnown(selected)
|
_selected = binding is { LootFileName.Length: > 0 } bound
|
||||||
? CanonicalName(selected)
|
? bound.LootFileName
|
||||||
: ByCharacter;
|
: ByCharacter;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Select(string? name)
|
public bool Select(string? name)
|
||||||
{
|
{
|
||||||
string normalized = name?.Trim() ?? string.Empty;
|
string normalized = name?.Trim() ?? string.Empty;
|
||||||
if (!IsKnown(normalized))
|
if (normalized.Length == 0)
|
||||||
return false;
|
return false;
|
||||||
_selected = CanonicalName(normalized);
|
if (normalized.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase))
|
||||||
_index.SelectedByCharacter[SelectionKey()] = _selected;
|
{
|
||||||
SaveIndex();
|
_selected = ByCharacter;
|
||||||
return true;
|
WriteBinding();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
string candidate = ToFileName(normalized);
|
||||||
|
if (VtankStorage.IsAvailable && VtankStorage.ReadText(candidate) is not null)
|
||||||
|
{
|
||||||
|
_selected = candidate;
|
||||||
|
WriteBinding();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Create(
|
public bool Create(
|
||||||
|
|
@ -97,32 +144,17 @@ internal sealed class MossTankLootProfileStore
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
LootProfileDocument? currentDocument = copyCurrent
|
string fileName = ToFileName(normalized);
|
||||||
? Read<LootProfileDocument>(CurrentKey())
|
var profile = new VtankLootProfile
|
||||||
: null;
|
|
||||||
var document = new LootProfileDocument
|
|
||||||
{
|
{
|
||||||
Rules = copyCurrent
|
Rules = copyCurrent ? current.ToList() : [],
|
||||||
? current.Select(LootRuleDocument.From).ToArray()
|
|
||||||
: [],
|
|
||||||
SalvageCombine = copyCurrent
|
SalvageCombine = copyCurrent
|
||||||
? (settings?.SalvageCombine.Clone()
|
? settings?.SalvageCombine.Clone() ?? new VtankSalvageCombineSettings()
|
||||||
?? currentDocument?.SalvageCombine?.Clone()
|
|
||||||
?? new VtankSalvageCombineSettings())
|
|
||||||
: new VtankSalvageCombineSettings(),
|
: new VtankSalvageCombineSettings(),
|
||||||
UnknownBlocks = copyCurrent
|
|
||||||
? currentDocument?.UnknownBlocks ?? []
|
|
||||||
: [],
|
|
||||||
};
|
};
|
||||||
Write(ProfileKey(normalized, byCharacter: false), document);
|
WriteUtl(fileName, profile);
|
||||||
if (!_index.Names.Contains(normalized, StringComparer.OrdinalIgnoreCase))
|
_selected = fileName;
|
||||||
_index.Names.Add(normalized);
|
WriteBinding();
|
||||||
_selected = _index.Names.First(entry => entry.Equals(
|
|
||||||
normalized,
|
|
||||||
StringComparison.OrdinalIgnoreCase));
|
|
||||||
_index.SelectedByCharacter[SelectionKey()] = _selected;
|
|
||||||
SaveIndex();
|
|
||||||
WriteLegacyExport(_selected, document);
|
|
||||||
notice = copyCurrent
|
notice = copyCurrent
|
||||||
? $"Copied loot rules to {_selected}."
|
? $"Copied loot rules to {_selected}."
|
||||||
: $"Created loot profile {_selected}.";
|
: $"Created loot profile {_selected}.";
|
||||||
|
|
@ -133,18 +165,23 @@ internal sealed class MossTankLootProfileStore
|
||||||
public bool LoadCurrent(List<LootRule> target, LootSettings? settings = null)
|
public bool LoadCurrent(List<LootRule> target, LootSettings? settings = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(target);
|
ArgumentNullException.ThrowIfNull(target);
|
||||||
LootProfileDocument? document = Read<LootProfileDocument>(CurrentKey());
|
SweepLegacyRosterIfNeeded();
|
||||||
if (document is null)
|
string fileName = CurrentFileName();
|
||||||
|
string? text = VtankStorage.IsAvailable ? VtankStorage.ReadText(fileName) : null;
|
||||||
|
if (text is null)
|
||||||
return false;
|
return false;
|
||||||
target.Clear();
|
if (!VtankLootProfileSerializer.TryRead(text, out VtankLootProfile profile, out string error))
|
||||||
foreach (LootRuleDocument rule in document.Rules ?? [])
|
|
||||||
target.Add(rule.ToRule());
|
|
||||||
if (settings is not null)
|
|
||||||
{
|
{
|
||||||
settings.SalvageCombine =
|
RecoveryNotice = MossTankProfileRecovery.Preserve(
|
||||||
document.SalvageCombine?.Clone()
|
_host, "loot", fileName, text, new FormatException(error));
|
||||||
?? new VtankSalvageCombineSettings();
|
_host.Log.Warn(RecoveryNotice);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
ApplyMossTankExpressions(profile);
|
||||||
|
target.Clear();
|
||||||
|
target.AddRange(profile.Rules);
|
||||||
|
if (settings is not null)
|
||||||
|
settings.SalvageCombine = profile.SalvageCombine.Clone();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,20 +197,22 @@ internal sealed class MossTankLootProfileStore
|
||||||
string normalized = name?.Trim() ?? string.Empty;
|
string normalized = name?.Trim() ?? string.Empty;
|
||||||
if (normalized.EndsWith(".utl", StringComparison.OrdinalIgnoreCase))
|
if (normalized.EndsWith(".utl", StringComparison.OrdinalIgnoreCase))
|
||||||
normalized = normalized[..^4];
|
normalized = normalized[..^4];
|
||||||
if (!IsKnown(normalized))
|
if (normalized.Length == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
string canonical = CanonicalName(normalized);
|
string fileName = normalized.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase)
|
||||||
string key = canonical.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase)
|
? CurrentFileName()
|
||||||
? ProfileKey(_characterName, byCharacter: true)
|
: ToFileName(normalized);
|
||||||
: ProfileKey(canonical, byCharacter: false);
|
string? text = VtankStorage.IsAvailable ? VtankStorage.ReadText(fileName) : null;
|
||||||
LootProfileDocument? document = Read<LootProfileDocument>(key);
|
if (text is null || !VtankLootProfileSerializer.TryRead(
|
||||||
if (document is null)
|
text, out VtankLootProfile profile, out _))
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyMossTankExpressions(profile);
|
||||||
target.Clear();
|
target.Clear();
|
||||||
foreach (LootRuleDocument rule in document.Rules ?? [])
|
target.AddRange(profile.Rules);
|
||||||
target.Add(rule.ToRule());
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,17 +220,45 @@ internal sealed class MossTankLootProfileStore
|
||||||
IReadOnlyList<LootRule> rules,
|
IReadOnlyList<LootRule> rules,
|
||||||
LootSettings? settings = null)
|
LootSettings? settings = null)
|
||||||
{
|
{
|
||||||
LootProfileDocument? existing = Read<LootProfileDocument>(CurrentKey());
|
string fileName = CurrentFileName();
|
||||||
var document = new LootProfileDocument
|
var profile = new VtankLootProfile
|
||||||
{
|
{
|
||||||
Rules = rules.Select(LootRuleDocument.From).ToArray(),
|
Rules = rules.ToList(),
|
||||||
SalvageCombine = settings?.SalvageCombine.Clone()
|
SalvageCombine = settings?.SalvageCombine.Clone() ?? new VtankSalvageCombineSettings(),
|
||||||
?? existing?.SalvageCombine?.Clone()
|
|
||||||
?? new VtankSalvageCombineSettings(),
|
|
||||||
UnknownBlocks = existing?.UnknownBlocks ?? [],
|
|
||||||
};
|
};
|
||||||
Write(CurrentKey(), document);
|
// Preserve UnknownBlocks/SourceVersion from whatever is already on
|
||||||
WriteLegacyExport(LegacyProfileName(), document);
|
// disk (a drop-in real .utl may carry blocks MossTank does not
|
||||||
|
// understand yet) rather than discarding them on every save.
|
||||||
|
string? existingText = VtankStorage.IsAvailable ? VtankStorage.ReadText(fileName) : null;
|
||||||
|
if (existingText is not null
|
||||||
|
&& VtankLootProfileSerializer.TryRead(existingText, out VtankLootProfile existing, out _))
|
||||||
|
{
|
||||||
|
profile.UnknownBlocks = existing.UnknownBlocks;
|
||||||
|
}
|
||||||
|
WriteUtl(fileName, profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round 3 item 10: deletes the currently selected named loot profile's
|
||||||
|
/// real <c>.utl</c> file, then falls back to <see cref="ByCharacter"/>.
|
||||||
|
/// Refuses for <see cref="ByCharacter"/> itself — there is no file to
|
||||||
|
/// delete, only a reset (see <see cref="ClearCurrent"/>), matching
|
||||||
|
/// <see cref="MossTankProfileStore.Delete"/>'s own contract.
|
||||||
|
/// </summary>
|
||||||
|
public bool Delete(out string notice)
|
||||||
|
{
|
||||||
|
if (_selected.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
notice = "'By char' is the built-in loot profile and cannot be deleted.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string fileName = _selected;
|
||||||
|
if (VtankStorage.IsAvailable)
|
||||||
|
VtankStorage.Delete(fileName);
|
||||||
|
_selected = ByCharacter;
|
||||||
|
WriteBinding();
|
||||||
|
notice = $"Deleted loot profile {StripUtl(fileName)}.";
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearCurrent(List<LootRule> target, LootSettings? settings = null)
|
public void ClearCurrent(List<LootRule> target, LootSettings? settings = null)
|
||||||
|
|
@ -239,47 +306,271 @@ internal sealed class MossTankLootProfileStore
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var document = new LootProfileDocument
|
string fileName = ToFileName(normalized);
|
||||||
{
|
WriteUtl(fileName, imported);
|
||||||
Rules = imported.Rules.Select(LootRuleDocument.From).ToArray(),
|
_selected = fileName;
|
||||||
SalvageCombine = imported.SalvageCombine.Clone(),
|
WriteBinding();
|
||||||
UnknownBlocks = imported.UnknownBlocks.Select(
|
|
||||||
VtankLootExtraBlockDocument.From).ToArray(),
|
|
||||||
};
|
|
||||||
Write(ProfileKey(normalized, byCharacter: false), document);
|
|
||||||
if (!_index.Names.Contains(normalized, StringComparer.OrdinalIgnoreCase))
|
|
||||||
_index.Names.Add(normalized);
|
|
||||||
_selected = _index.Names.First(entry => entry.Equals(
|
|
||||||
normalized,
|
|
||||||
StringComparison.OrdinalIgnoreCase));
|
|
||||||
_index.SelectedByCharacter[SelectionKey()] = _selected;
|
|
||||||
SaveIndex();
|
|
||||||
target.Clear();
|
target.Clear();
|
||||||
target.AddRange(imported.Rules);
|
target.AddRange(imported.Rules);
|
||||||
if (settings is not null)
|
if (settings is not null)
|
||||||
settings.SalvageCombine = imported.SalvageCombine.Clone();
|
settings.SalvageCombine = imported.SalvageCombine.Clone();
|
||||||
WriteLegacyExport(_selected, document);
|
|
||||||
notice = $"Imported VTClassic loot profile {_selected}.";
|
notice = $"Imported VTClassic loot profile {_selected}.";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsKnown(string? name) => name is not null
|
// ------------------------------------------------------------------
|
||||||
&& (name.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase)
|
// Legacy JSON -> .utl migration (round 3 item 9).
|
||||||
|| _index.Names.Contains(name, StringComparer.OrdinalIgnoreCase));
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
private string CanonicalName(string name) => name.Equals(
|
/// <summary>
|
||||||
ByCharacter,
|
/// Round 3 item 9: a ONE-TIME sweep (guarded by <see cref="_rosterSwept"/>)
|
||||||
StringComparison.OrdinalIgnoreCase)
|
/// over the pre-cutover roster at <see cref="LegacyRosterKey"/>. Unlike
|
||||||
? ByCharacter
|
/// Settings/Meta/Route's own per-selection migration (which only ever
|
||||||
: _index.Names.First(entry => entry.Equals(
|
/// converted whichever ONE profile happened to be selected), this
|
||||||
name,
|
/// single pass converts EVERY named profile the roster lists AND this
|
||||||
StringComparison.OrdinalIgnoreCase));
|
/// character's own "By char" JSON document, since the roster was still
|
||||||
|
/// the live mechanism right up to this cutover — there is no separate
|
||||||
|
/// "already migrated one, sweep the rest" split here.
|
||||||
|
/// </summary>
|
||||||
|
private void SweepLegacyRosterIfNeeded()
|
||||||
|
{
|
||||||
|
if (_rosterSwept)
|
||||||
|
return;
|
||||||
|
_rosterSwept = true;
|
||||||
|
if (!_host.Storage.IsAvailable)
|
||||||
|
return;
|
||||||
|
|
||||||
private string CurrentKey() => _selected.Equals(
|
// This character's own "By char" JSON document, if not already a
|
||||||
ByCharacter,
|
// real .utl file.
|
||||||
StringComparison.OrdinalIgnoreCase)
|
string byCharacterFileName = CurrentFileName();
|
||||||
? ProfileKey(_characterName, byCharacter: true)
|
if (VtankStorage.ReadText(byCharacterFileName) is null)
|
||||||
: ProfileKey(_selected, byCharacter: false);
|
{
|
||||||
|
LootProfileDocument? byCharacter = ReadLegacyJson(
|
||||||
|
ProfileKey(_characterName, byCharacter: true));
|
||||||
|
if (byCharacter is not null)
|
||||||
|
{
|
||||||
|
WriteUtl(byCharacterFileName, byCharacter.ToVtankProfile());
|
||||||
|
_host.Storage.Delete(ProfileKey(_characterName, byCharacter: true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LegacyRosterDocument? roster = ReadLegacyRoster();
|
||||||
|
if (roster?.Names is not { Count: > 0 } names)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var remaining = new List<string>();
|
||||||
|
int migrated = 0;
|
||||||
|
foreach (string rawName in names)
|
||||||
|
{
|
||||||
|
string name = (rawName ?? string.Empty).Trim();
|
||||||
|
if (name.Length == 0 || name.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
continue; // stale/invalid row; drop it rather than loop on it forever.
|
||||||
|
|
||||||
|
string legacyKey = ProfileKey(name, byCharacter: false);
|
||||||
|
LootProfileDocument? legacy = ReadLegacyJson(legacyKey);
|
||||||
|
if (legacy is null)
|
||||||
|
continue; // already converted (or never existed); drop the row.
|
||||||
|
|
||||||
|
string fileName = ToFileName(name);
|
||||||
|
if (VtankStorage.ReadText(fileName) is null)
|
||||||
|
WriteUtl(fileName, legacy.ToVtankProfile());
|
||||||
|
_host.Storage.Delete(legacyKey);
|
||||||
|
migrated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (migrated == 0)
|
||||||
|
return;
|
||||||
|
if (remaining.Count > 0)
|
||||||
|
{
|
||||||
|
_host.Storage.WriteText(
|
||||||
|
LegacyRosterKey,
|
||||||
|
JsonSerializer.Serialize(new LegacyRosterDocument { Names = remaining }, Options));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_host.Storage.Delete(LegacyRosterKey);
|
||||||
|
}
|
||||||
|
_host.Log.Warn($"Migrated {migrated} legacy MossTank named loot profile(s) from the old roster.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private LegacyRosterDocument? ReadLegacyRoster()
|
||||||
|
{
|
||||||
|
string? json = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
json = _host.Storage.ReadText(LegacyRosterKey);
|
||||||
|
return string.IsNullOrWhiteSpace(json)
|
||||||
|
? null
|
||||||
|
: JsonSerializer.Deserialize<LegacyRosterDocument>(json, Options);
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
RecoveryNotice = MossTankProfileRecovery.Preserve(
|
||||||
|
_host, "loot", LegacyRosterKey, json, error);
|
||||||
|
_host.Log.Warn(RecoveryNotice);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LootProfileDocument? ReadLegacyJson(string key)
|
||||||
|
{
|
||||||
|
if (!_host.Storage.IsAvailable)
|
||||||
|
return null;
|
||||||
|
string? json = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
json = _host.Storage.ReadText(key);
|
||||||
|
return string.IsNullOrWhiteSpace(json)
|
||||||
|
? null
|
||||||
|
: JsonSerializer.Deserialize<LootProfileDocument>(json, Options);
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
RecoveryNotice = MossTankProfileRecovery.Preserve(_host, "loot", key, json, error);
|
||||||
|
_host.Log.Warn(RecoveryNotice);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// File naming, storage plumbing.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
private string CurrentFileName() => _selected.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? VtankProfileDirectory.AutoCharacterFileName(_characterName, Server, "utl")
|
||||||
|
: _selected;
|
||||||
|
|
||||||
|
private static string ToFileName(string bareName) =>
|
||||||
|
bareName.EndsWith(".utl", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? bareName
|
||||||
|
: bareName + ".utl";
|
||||||
|
|
||||||
|
private void WriteBinding()
|
||||||
|
{
|
||||||
|
if (!CanBindFiles || !VtankStorage.IsAvailable)
|
||||||
|
return;
|
||||||
|
VtankProfileDirectory.VtankCharacterBinding existing =
|
||||||
|
VtankProfileDirectory.TryReadCharacterBinding(VtankStorage, _characterName, Server)
|
||||||
|
?? new VtankProfileDirectory.VtankCharacterBinding(
|
||||||
|
string.Empty, CurrentFileName(), string.Empty, null);
|
||||||
|
VtankProfileDirectory.WriteCharacterBinding(
|
||||||
|
VtankStorage,
|
||||||
|
_characterName,
|
||||||
|
Server,
|
||||||
|
existing with { LootFileName = CurrentFileName() });
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteUtl(string fileName, VtankLootProfile profile)
|
||||||
|
{
|
||||||
|
if (!VtankStorage.IsAvailable)
|
||||||
|
return;
|
||||||
|
AttachMossTankExpressions(profile);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
VtankStorage.WriteText(fileName, VtankLootProfileSerializer.Write(profile));
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
_host.Log.Warn($"MossTank loot profile could not be saved: {error.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// MossTank rule-expression preservation (round 3 item 9): VTClassic's
|
||||||
|
// real .utl requirement grammar cannot express an arbitrary MossTank
|
||||||
|
// Expression string at all (VtankLootProfileSerializer.ExportRequirements
|
||||||
|
// replaces an empty VtankRequirements list with a "safely disabled"
|
||||||
|
// placeholder requirement for VTClassic compatibility) — fine when
|
||||||
|
// .utl was only ever a courtesy export mirror alongside the
|
||||||
|
// authoritative JSON store, but silently DESTRUCTIVE now that .utl is
|
||||||
|
// the SOLE store: every MossTank-authored rule's Expression would be
|
||||||
|
// permanently lost on its very first save/reload cycle. Preserved
|
||||||
|
// instead in a MossTank-owned length-delimited block
|
||||||
|
// (VtankLootProfileSerializer's own UnknownBlocks mechanism already
|
||||||
|
// round-trips any block type it doesn't recognize as "SalvageCombine"
|
||||||
|
// verbatim — exactly the "a foreign reader keeps what it doesn't
|
||||||
|
// understand" contract this needs) so a real VTClassic reading the
|
||||||
|
// same file simply ignores it as an unknown block, the same as any
|
||||||
|
// other extension block.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
private const string MossTankRulesBlockType = "MossTankRuleExpressions";
|
||||||
|
|
||||||
|
private static void AttachMossTankExpressions(VtankLootProfile profile)
|
||||||
|
{
|
||||||
|
profile.UnknownBlocks.RemoveAll(static block =>
|
||||||
|
string.Equals(block.Type, MossTankRulesBlockType, StringComparison.Ordinal));
|
||||||
|
|
||||||
|
var payload = new StringBuilder();
|
||||||
|
payload.Append(profile.Rules.Count.ToString(CultureInfo.InvariantCulture)).Append("\r\n");
|
||||||
|
foreach (LootRule rule in profile.Rules)
|
||||||
|
{
|
||||||
|
// A rule with real VtankRequirements (imported from a genuine
|
||||||
|
// VTClassic file, never touched by MossTank's own editor) has
|
||||||
|
// nothing of ours to preserve — record an empty slot so load
|
||||||
|
// leaves its VtankRequirements-derived state alone.
|
||||||
|
string expression = rule.VtankRequirements.Count > 0 ? string.Empty : rule.Expression;
|
||||||
|
payload.Append(expression.Length.ToString(CultureInfo.InvariantCulture)).Append("\r\n");
|
||||||
|
payload.Append(expression);
|
||||||
|
}
|
||||||
|
profile.UnknownBlocks.Add(new VtankLootExtraBlock
|
||||||
|
{
|
||||||
|
Type = MossTankRulesBlockType,
|
||||||
|
Payload = payload.ToString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyMossTankExpressions(VtankLootProfile profile)
|
||||||
|
{
|
||||||
|
VtankLootExtraBlock? block = profile.UnknownBlocks.FirstOrDefault(candidate =>
|
||||||
|
string.Equals(candidate.Type, MossTankRulesBlockType, StringComparison.Ordinal));
|
||||||
|
if (block is null)
|
||||||
|
return;
|
||||||
|
profile.UnknownBlocks.Remove(block);
|
||||||
|
|
||||||
|
string payload = block.Payload ?? string.Empty;
|
||||||
|
int position = 0;
|
||||||
|
if (!TryReadPayloadLine(payload, ref position, out string countText)
|
||||||
|
|| !int.TryParse(countText, NumberStyles.Integer, CultureInfo.InvariantCulture, out int count))
|
||||||
|
{
|
||||||
|
return; // Corrupt/foreign block reusing our type name; ignore rather than throw.
|
||||||
|
}
|
||||||
|
int limit = Math.Min(count, profile.Rules.Count);
|
||||||
|
for (int index = 0; index < limit; index++)
|
||||||
|
{
|
||||||
|
if (!TryReadPayloadLine(payload, ref position, out string lengthText)
|
||||||
|
|| !int.TryParse(lengthText, NumberStyles.Integer, CultureInfo.InvariantCulture, out int length)
|
||||||
|
|| length < 0
|
||||||
|
|| length > payload.Length - position)
|
||||||
|
{
|
||||||
|
return; // Truncated/corrupt; stop applying rather than throw.
|
||||||
|
}
|
||||||
|
string expression = payload.Substring(position, length);
|
||||||
|
position += length;
|
||||||
|
if (expression.Length == 0)
|
||||||
|
continue;
|
||||||
|
profile.Rules[index].Expression = expression;
|
||||||
|
profile.Rules[index].VtankRequirements.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadPayloadLine(string text, ref int position, out string line)
|
||||||
|
{
|
||||||
|
if (position > text.Length)
|
||||||
|
{
|
||||||
|
line = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int start = position;
|
||||||
|
while (position < text.Length && text[position] is not ('\r' or '\n'))
|
||||||
|
position++;
|
||||||
|
line = text[start..position];
|
||||||
|
if (position < text.Length && text[position] == '\r')
|
||||||
|
position++;
|
||||||
|
if (position < text.Length && text[position] == '\n')
|
||||||
|
position++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static string ProfileKey(string value, bool byCharacter)
|
private static string ProfileKey(string value, bool byCharacter)
|
||||||
{
|
{
|
||||||
|
|
@ -290,90 +581,13 @@ internal sealed class MossTankLootProfileStore
|
||||||
return $"profiles/loot/{hash}.json";
|
return $"profiles/loot/{hash}.json";
|
||||||
}
|
}
|
||||||
|
|
||||||
private string SelectionKey() => string.IsNullOrWhiteSpace(_characterName)
|
// ------------------------------------------------------------------
|
||||||
? "_default"
|
// Migration-only shapes: the OLD JSON roster and per-profile document,
|
||||||
: _characterName;
|
// kept solely so SweepLegacyRosterIfNeeded can recover them once.
|
||||||
|
// Nothing else in this file writes either shape again.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
private T? Read<T>(string key) where T : class
|
private sealed class LegacyRosterDocument
|
||||||
{
|
|
||||||
if (!_host.Storage.IsAvailable)
|
|
||||||
return null;
|
|
||||||
string? json = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
json = _host.Storage.ReadText(key);
|
|
||||||
return string.IsNullOrWhiteSpace(json)
|
|
||||||
? null
|
|
||||||
: JsonSerializer.Deserialize<T>(json, Options);
|
|
||||||
}
|
|
||||||
catch (Exception error)
|
|
||||||
{
|
|
||||||
RecoveryNotice = MossTankProfileRecovery.Preserve(
|
|
||||||
_host,
|
|
||||||
"loot",
|
|
||||||
key,
|
|
||||||
json,
|
|
||||||
error);
|
|
||||||
_host.Log.Warn(RecoveryNotice);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Write<T>(string key, T document)
|
|
||||||
{
|
|
||||||
if (!_host.Storage.IsAvailable)
|
|
||||||
return;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_host.Storage.WriteText(key, JsonSerializer.Serialize(document, Options));
|
|
||||||
}
|
|
||||||
catch (Exception error)
|
|
||||||
{
|
|
||||||
_host.Log.Warn($"MossTank loot profile could not be saved: {error.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SaveIndex() => Write(IndexKey, _index);
|
|
||||||
|
|
||||||
private void WriteLegacyExport(string name, LootProfileDocument document)
|
|
||||||
{
|
|
||||||
if (!_host.Storage.IsAvailable)
|
|
||||||
return;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_host.Storage.WriteText(
|
|
||||||
$"exports/{LegacyFileName(name)}.utl",
|
|
||||||
VtankLootProfileSerializer.Write(document.ToVtankProfile()));
|
|
||||||
}
|
|
||||||
catch (Exception error)
|
|
||||||
{
|
|
||||||
_host.Log.Warn(
|
|
||||||
$"MossTank VTClassic loot export could not be saved: {error.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string LegacyProfileName() => _selected.Equals(
|
|
||||||
ByCharacter,
|
|
||||||
StringComparison.OrdinalIgnoreCase)
|
|
||||||
? string.IsNullOrWhiteSpace(_characterName)
|
|
||||||
? ByCharacter
|
|
||||||
: _characterName
|
|
||||||
: _selected;
|
|
||||||
|
|
||||||
private static string LegacyFileName(string name)
|
|
||||||
{
|
|
||||||
char[] invalid = Path.GetInvalidFileNameChars();
|
|
||||||
var result = new StringBuilder(name.Length);
|
|
||||||
foreach (char value in name.Trim())
|
|
||||||
{
|
|
||||||
result.Append(value is '/' or '\\' || invalid.Contains(value)
|
|
||||||
? '_'
|
|
||||||
: value);
|
|
||||||
}
|
|
||||||
return result.Length == 0 ? "Loot" : result.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class IndexDocument
|
|
||||||
{
|
{
|
||||||
public int Version { get; set; } = 1;
|
public int Version { get; set; } = 1;
|
||||||
public List<string> Names { get; set; } = [];
|
public List<string> Names { get; set; } = [];
|
||||||
|
|
|
||||||
|
|
@ -5,47 +5,76 @@ using AcDream.Plugin.Abstractions;
|
||||||
|
|
||||||
namespace AcDream.Plugins.MossTank;
|
namespace AcDream.Plugins.MossTank;
|
||||||
|
|
||||||
/// <summary>Independent VTank-style By-char/named Meta profile lifetime.</summary>
|
/// <summary>
|
||||||
|
/// VTank-compatible Meta profile lifecycle. <c>.af</c> (metaf's human-
|
||||||
|
/// readable grammar, see <see cref="MetafSerializer"/>) is the ONLY storage
|
||||||
|
/// and authoring format — real VTank's own <c>ac()</c> meta-profile picker
|
||||||
|
/// (<c>docs/research/vtank-kb/01-settings-and-profiles.md</c> section 3)
|
||||||
|
/// seeds <c>[None]</c>/<c>[By char]</c> then every non-<c>--</c> file: named
|
||||||
|
/// Meta profiles are plain shared files (unlike the Settings picker, there
|
||||||
|
/// is no per-character <c>[Char] suffix</c> sub-profile carve-out here),
|
||||||
|
/// while the per-character auto file uses VTank's <c>--Name_Server.af</c>
|
||||||
|
/// naming and is therefore hidden from that listing by construction.
|
||||||
|
/// </summary>
|
||||||
internal sealed class MossTankMetaProfileStore
|
internal sealed class MossTankMetaProfileStore
|
||||||
{
|
{
|
||||||
public const string ByCharacter = "By char";
|
public const string ByCharacter = "By char";
|
||||||
private const string IndexKey = "profiles/meta/index.json";
|
|
||||||
private static readonly JsonSerializerOptions Options = new()
|
// The pre-cutover roster (round 3 item 2): before the .af cutover this
|
||||||
{
|
// store kept a flat Names list here (unlike the settings roster, named
|
||||||
WriteIndented = true,
|
// Meta profiles were never owner-scoped — one shared, globally-hashed
|
||||||
PropertyNameCaseInsensitive = true,
|
// key per name). The cutover stopped reading this key at all, orphaning
|
||||||
};
|
// every named profile except whichever one a character had selected
|
||||||
|
// (that one alone still converts, via MigrateLegacyIfNeeded). Read-only:
|
||||||
|
// consulted ONLY by the one-time SweepLegacyRosterIfNeeded.
|
||||||
|
private const string LegacyRosterKey = "profiles/meta/index.json";
|
||||||
|
|
||||||
private readonly IPluginHost _host;
|
private readonly IPluginHost _host;
|
||||||
private IndexDocument _index;
|
|
||||||
private string _character = string.Empty;
|
private string _character = string.Empty;
|
||||||
private string _selected = ByCharacter;
|
private string _selected = ByCharacter;
|
||||||
|
private string? _pendingLegacyBareName;
|
||||||
|
private bool _rosterSwept;
|
||||||
|
|
||||||
public MossTankMetaProfileStore(IPluginHost host)
|
public MossTankMetaProfileStore(IPluginHost host)
|
||||||
{
|
{
|
||||||
_host = host ?? throw new ArgumentNullException(nameof(host));
|
_host = host ?? throw new ArgumentNullException(nameof(host));
|
||||||
_index = Read<IndexDocument>(IndexKey) ?? new IndexDocument();
|
|
||||||
_index.Names ??= [];
|
|
||||||
_index.SelectedByCharacter = new Dictionary<string, string>(
|
|
||||||
_index.SelectedByCharacter ?? [],
|
|
||||||
StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<string> Names => _index.Names ??= [];
|
/// <summary>
|
||||||
|
/// The bare name a user typed to select/create this profile (the file
|
||||||
private Dictionary<string, string> SelectedByCharacter =>
|
/// name minus its <c>.af</c> extension) — internal identity (<see cref="_selected"/>)
|
||||||
_index.SelectedByCharacter ??= new Dictionary<string, string>(
|
/// always carries the real, on-disk file name; only display strips it.
|
||||||
StringComparer.OrdinalIgnoreCase);
|
/// </summary>
|
||||||
|
public string Selected => StripAf(_selected);
|
||||||
public string Selected => _selected;
|
|
||||||
public string? RecoveryNotice { get; private set; }
|
public string? RecoveryNotice { get; private set; }
|
||||||
public IReadOnlyList<string> AvailableNames => new[] { ByCharacter }
|
public string? SaveNotice { get; private set; }
|
||||||
.Concat(Names)
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
private string Server => _host.Automation.Character.WorldName;
|
||||||
.OrderBy(static name => name.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase)
|
private IPluginStorage VtankStorage => _host.VtankProfiles;
|
||||||
? 0 : 1)
|
private bool CanBindFiles => _character.Length > 0 && Server.Length > 0;
|
||||||
.ThenBy(static name => name, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToArray();
|
private static string StripAf(string name) => name.Equals(
|
||||||
|
ByCharacter, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? name
|
||||||
|
: name.EndsWith(".af", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? name[..^3]
|
||||||
|
: name;
|
||||||
|
|
||||||
|
public IReadOnlyList<string> AvailableNames
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var names = new List<string> { ByCharacter };
|
||||||
|
foreach (VtankProfileDirectory.ProfileEntry entry in
|
||||||
|
VtankProfileDirectory.ListMetaProfiles(VtankStorage))
|
||||||
|
{
|
||||||
|
if (entry.FileName.Length == 0)
|
||||||
|
continue; // VTank's own "[None]"/"[By char]" sentinels.
|
||||||
|
names.Add(StripAf(entry.FileName));
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool BindCharacter(string? characterName)
|
public bool BindCharacter(string? characterName)
|
||||||
{
|
{
|
||||||
|
|
@ -55,33 +84,106 @@ internal sealed class MossTankMetaProfileStore
|
||||||
if (normalized.Equals(_character, StringComparison.OrdinalIgnoreCase))
|
if (normalized.Equals(_character, StringComparison.OrdinalIgnoreCase))
|
||||||
return false;
|
return false;
|
||||||
_character = normalized;
|
_character = normalized;
|
||||||
_selected = SelectedByCharacter.TryGetValue(
|
_pendingLegacyBareName = null;
|
||||||
CharacterKey(),
|
VtankProfileDirectory.VtankCharacterBinding? binding = CanBindFiles
|
||||||
out string? selected)
|
? VtankProfileDirectory.TryReadCharacterBinding(VtankStorage, _character, Server)
|
||||||
&& IsKnown(selected)
|
: null;
|
||||||
? Canonical(selected)
|
_selected = binding is { MetaFileName.Length: > 0 } bound
|
||||||
: ByCharacter;
|
? bound.MetaFileName
|
||||||
|
: ByCharacter;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MetaProfile LoadCurrent() =>
|
public MetaProfile LoadCurrent()
|
||||||
Read<MetaProfile>(CurrentKey()) ?? new MetaProfile();
|
|
||||||
|
|
||||||
public void SaveCurrent(MetaProfile profile)
|
|
||||||
{
|
{
|
||||||
Write(CurrentKey(), profile);
|
SweepLegacyRosterIfNeeded();
|
||||||
WriteLegacyExport(LegacyProfileName(), profile);
|
MigrateLegacyIfNeeded();
|
||||||
|
string fileName = CurrentFileName();
|
||||||
|
string? text = VtankStorage.IsAvailable ? VtankStorage.ReadText(fileName) : null;
|
||||||
|
if (text is null)
|
||||||
|
return new MetaProfile();
|
||||||
|
if (!MetafSerializer.TryLoadMeta(text, _host.Automation.Spells, out MetaProfile profile, out string error))
|
||||||
|
{
|
||||||
|
RecoveryNotice = MossTankProfileRecovery.Preserve(
|
||||||
|
_host, "meta", fileName, text, new FormatException(error));
|
||||||
|
_host.Log.Warn(RecoveryNotice);
|
||||||
|
return new MetaProfile();
|
||||||
|
}
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves <paramref name="profile"/> as this selection's <c>.af</c> file.
|
||||||
|
/// Real VTank/metaf has no way to represent <see cref="MetaRule.Enabled"/>
|
||||||
|
/// <see langword="false"/> (a MossTank-only extension) — rather than
|
||||||
|
/// silently dropping a disabled rule, the save is refused entirely (the
|
||||||
|
/// file on disk keeps its last good content) and <see cref="SaveNotice"/>
|
||||||
|
/// carries a user-visible reason. Returns <see langword="true"/> only
|
||||||
|
/// when the file was actually written.
|
||||||
|
/// </summary>
|
||||||
|
public bool SaveCurrent(MetaProfile profile)
|
||||||
|
{
|
||||||
|
string fileName = CurrentFileName();
|
||||||
|
string text;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
text = MetafSerializer.SaveMeta(profile);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException error)
|
||||||
|
{
|
||||||
|
SaveNotice = $"Meta profile '{fileName}' was NOT saved: {error.Message}";
|
||||||
|
_host.Log.Warn(SaveNotice);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!VtankStorage.IsAvailable)
|
||||||
|
return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
VtankStorage.WriteText(fileName, text);
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
SaveNotice = $"Meta profile '{fileName}' could not be saved: {error.Message}";
|
||||||
|
_host.Log.Warn(SaveNotice);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SaveNotice = null;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Select(string? name)
|
public bool Select(string? name)
|
||||||
{
|
{
|
||||||
string normalized = Normalize(name);
|
string normalized = Normalize(name);
|
||||||
if (!IsKnown(normalized))
|
if (normalized.Length == 0)
|
||||||
return false;
|
return false;
|
||||||
_selected = Canonical(normalized);
|
if (normalized.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase))
|
||||||
SelectedByCharacter[CharacterKey()] = _selected;
|
{
|
||||||
SaveIndex();
|
_selected = ByCharacter;
|
||||||
return true;
|
_pendingLegacyBareName = null;
|
||||||
|
WriteBinding();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
string plain = normalized.EndsWith(".af", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? normalized
|
||||||
|
: normalized + ".af";
|
||||||
|
if (VtankStorage.IsAvailable && VtankStorage.ReadText(plain) is not null)
|
||||||
|
{
|
||||||
|
_selected = plain;
|
||||||
|
_pendingLegacyBareName = null;
|
||||||
|
WriteBinding();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_host.Storage.IsAvailable
|
||||||
|
&& _host.Storage.ReadText(LegacyNamedKey(normalized)) is not null)
|
||||||
|
{
|
||||||
|
_selected = plain;
|
||||||
|
_pendingLegacyBareName = normalized;
|
||||||
|
WriteBinding();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Create(
|
public bool Create(
|
||||||
|
|
@ -97,19 +199,18 @@ internal sealed class MossTankMetaProfileStore
|
||||||
notice = "Enter a unique Meta profile name (1-64 characters).";
|
notice = "Enter a unique Meta profile name (1-64 characters).";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
MetaProfile document = copyCurrent
|
string fileName = normalized.EndsWith(".af", StringComparison.OrdinalIgnoreCase)
|
||||||
? Clone(current)
|
? normalized
|
||||||
: new MetaProfile();
|
: normalized + ".af";
|
||||||
Write(NamedKey(normalized), document);
|
MetaProfile document = copyCurrent ? Clone(current) : new MetaProfile();
|
||||||
if (!Names.Contains(normalized, StringComparer.OrdinalIgnoreCase))
|
if (!SaveTo(fileName, document, out notice))
|
||||||
Names.Add(normalized);
|
return false;
|
||||||
_selected = normalized;
|
_selected = fileName;
|
||||||
SelectedByCharacter[CharacterKey()] = normalized;
|
_pendingLegacyBareName = null;
|
||||||
SaveIndex();
|
WriteBinding();
|
||||||
WriteLegacyExport(normalized, document);
|
|
||||||
notice = copyCurrent
|
notice = copyCurrent
|
||||||
? $"Copied Meta profile to {normalized}."
|
? $"Copied Meta profile to {fileName}."
|
||||||
: $"Created Meta profile {normalized}.";
|
: $"Created Meta profile {fileName}.";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,20 +240,48 @@ internal sealed class MossTankMetaProfileStore
|
||||||
notice = $"VTank Meta file '{normalized}.met' was not found in imports.";
|
notice = $"VTank Meta file '{normalized}.met' was not found in imports.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!VtankMetaProfileSerializer.TryLoad(source, out profile, out string error))
|
if (!VtankMetaProfileSerializer.TryLoad(
|
||||||
|
source, _host.Automation.Spells, out profile, out string error))
|
||||||
{
|
{
|
||||||
notice = $"Could not import {Path.GetFileName(key)}: {error}";
|
notice = $"Could not import {Path.GetFileName(key)}: {error}";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!Names.Contains(normalized, StringComparer.OrdinalIgnoreCase))
|
string fileName = normalized.EndsWith(".af", StringComparison.OrdinalIgnoreCase)
|
||||||
Names.Add(normalized);
|
? normalized
|
||||||
_selected = Names.First(existing => existing.Equals(
|
: normalized + ".af";
|
||||||
normalized,
|
if (!SaveTo(fileName, profile, out string saveNotice))
|
||||||
StringComparison.OrdinalIgnoreCase));
|
{
|
||||||
SelectedByCharacter[CharacterKey()] = _selected;
|
notice = saveNotice;
|
||||||
SaveIndex();
|
return false;
|
||||||
SaveCurrent(profile);
|
}
|
||||||
notice = $"Imported VTank Meta profile {_selected}.";
|
_selected = fileName;
|
||||||
|
_pendingLegacyBareName = null;
|
||||||
|
WriteBinding();
|
||||||
|
notice = $"Imported VTank Meta profile {fileName}.";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round 3 item 10: deletes the currently selected named Meta profile's
|
||||||
|
/// real <c>.af</c> file, then falls back to <see cref="ByCharacter"/>.
|
||||||
|
/// Refuses for <see cref="ByCharacter"/> itself — there is no file to
|
||||||
|
/// delete, only a reset (see <see cref="ClearCurrent"/>), matching
|
||||||
|
/// <see cref="MossTankProfileStore.Delete"/>'s own contract.
|
||||||
|
/// </summary>
|
||||||
|
public bool Delete(out string notice)
|
||||||
|
{
|
||||||
|
if (_selected.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
notice = "'By char' is the built-in Meta profile and cannot be deleted.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string fileName = _selected;
|
||||||
|
if (VtankStorage.IsAvailable)
|
||||||
|
VtankStorage.Delete(fileName);
|
||||||
|
_selected = ByCharacter;
|
||||||
|
_pendingLegacyBareName = null;
|
||||||
|
WriteBinding();
|
||||||
|
notice = $"Deleted Meta profile {fileName}.";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,124 +292,237 @@ internal sealed class MossTankMetaProfileStore
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string CurrentKey() => _selected.Equals(
|
// ------------------------------------------------------------------
|
||||||
ByCharacter,
|
// Legacy JSON -> .af migration.
|
||||||
StringComparison.OrdinalIgnoreCase)
|
// ------------------------------------------------------------------
|
||||||
? $"profiles/meta/by-character/{Hash(_character)}.json"
|
|
||||||
: NamedKey(_selected);
|
|
||||||
|
|
||||||
private static string NamedKey(string name) =>
|
/// <summary>
|
||||||
$"profiles/meta/named/{Hash(name)}.json";
|
/// Round 3 item 2: a ONE-TIME sweep (guarded by <see cref="_rosterSwept"/>)
|
||||||
|
/// over the pre-cutover roster at <see cref="LegacyRosterKey"/>. Unlike
|
||||||
private string CharacterKey() =>
|
/// <see cref="MigrateLegacyIfNeeded"/> — which only ever converts the
|
||||||
string.IsNullOrWhiteSpace(_character) ? "anonymous" : _character;
|
/// ONE currently-selected profile — this converts every OTHER named
|
||||||
|
/// Meta profile the old roster still lists. Named Meta profiles were
|
||||||
private bool IsKnown(string name) => name.Equals(
|
/// never owner-scoped (one shared, globally-hashed key per name), so —
|
||||||
ByCharacter,
|
/// unlike the settings roster — every entry converts regardless of
|
||||||
StringComparison.OrdinalIgnoreCase)
|
/// which character is currently bound.
|
||||||
|| Names.Contains(name, StringComparer.OrdinalIgnoreCase);
|
/// </summary>
|
||||||
|
private void SweepLegacyRosterIfNeeded()
|
||||||
private string Canonical(string name) => name.Equals(
|
|
||||||
ByCharacter,
|
|
||||||
StringComparison.OrdinalIgnoreCase)
|
|
||||||
? ByCharacter
|
|
||||||
: Names.First(existing => existing.Equals(
|
|
||||||
name,
|
|
||||||
StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
private void SaveIndex() => Write(IndexKey, _index);
|
|
||||||
|
|
||||||
private void WriteLegacyExport(string name, MetaProfile profile)
|
|
||||||
{
|
{
|
||||||
|
if (_rosterSwept)
|
||||||
|
return;
|
||||||
|
_rosterSwept = true;
|
||||||
if (!_host.Storage.IsAvailable)
|
if (!_host.Storage.IsAvailable)
|
||||||
return;
|
return;
|
||||||
try
|
List<string>? names = ReadRosterNames();
|
||||||
|
if (names is not { Count: > 0 })
|
||||||
|
return;
|
||||||
|
|
||||||
|
var remaining = new List<string>();
|
||||||
|
int migrated = 0;
|
||||||
|
foreach (string rawName in names)
|
||||||
|
{
|
||||||
|
string name = (rawName ?? string.Empty).Trim();
|
||||||
|
if (name.Length == 0 || name.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
continue; // stale/invalid row; drop it rather than loop on it forever.
|
||||||
|
|
||||||
|
string legacyKey = LegacyNamedKey(name);
|
||||||
|
MetaProfile? legacy = ReadLegacyJson(legacyKey);
|
||||||
|
if (legacy is null)
|
||||||
|
continue; // already converted (or never existed); drop the row.
|
||||||
|
|
||||||
|
string fileName = name.EndsWith(".af", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? name
|
||||||
|
: name + ".af";
|
||||||
|
if (VtankStorage.IsAvailable && VtankStorage.ReadText(fileName) is null)
|
||||||
|
{
|
||||||
|
if (!SaveTo(fileName, legacy, out string notice))
|
||||||
|
{
|
||||||
|
// Representational loss (a disabled rule) — same gate as
|
||||||
|
// MigrateLegacyIfNeeded: keep the row so this can be
|
||||||
|
// retried once the user resolves it.
|
||||||
|
_host.Log.Warn($"MossTank could not sweep legacy Meta profile '{name}': {notice}");
|
||||||
|
remaining.Add(name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_host.Storage.Delete(legacyKey);
|
||||||
|
migrated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (migrated == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (remaining.Count > 0)
|
||||||
{
|
{
|
||||||
_host.Storage.WriteText(
|
_host.Storage.WriteText(
|
||||||
$"exports/{LegacyFileName(name)}.met",
|
LegacyRosterKey,
|
||||||
VtankMetaProfileSerializer.Save(profile));
|
JsonSerializer.Serialize(new LegacyRosterDocument { Names = remaining }, JsonOptions));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_host.Storage.Delete(LegacyRosterKey);
|
||||||
|
}
|
||||||
|
_host.Log.Warn($"Migrated {migrated} legacy MossTank named Meta profile(s) from the old roster.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string>? ReadRosterNames()
|
||||||
|
{
|
||||||
|
string? json = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
json = _host.Storage.ReadText(LegacyRosterKey);
|
||||||
|
return string.IsNullOrWhiteSpace(json)
|
||||||
|
? null
|
||||||
|
: JsonSerializer.Deserialize<LegacyRosterDocument>(json, JsonOptions)?.Names;
|
||||||
}
|
}
|
||||||
catch (Exception error)
|
catch (Exception error)
|
||||||
{
|
{
|
||||||
_host.Log.Warn(
|
RecoveryNotice = MossTankProfileRecovery.Preserve(
|
||||||
$"MossTank VTank Meta export could not be saved: {error.Message}");
|
_host, "meta", LegacyRosterKey, json, error);
|
||||||
|
_host.Log.Warn(RecoveryNotice);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string LegacyProfileName() => _selected.Equals(
|
private void MigrateLegacyIfNeeded()
|
||||||
ByCharacter,
|
|
||||||
StringComparison.OrdinalIgnoreCase)
|
|
||||||
? string.IsNullOrWhiteSpace(_character) ? ByCharacter : _character
|
|
||||||
: _selected;
|
|
||||||
|
|
||||||
private static string LegacyFileName(string name)
|
|
||||||
{
|
{
|
||||||
char[] invalid = Path.GetInvalidFileNameChars();
|
string fileName = CurrentFileName();
|
||||||
var result = new StringBuilder(name.Length);
|
if (!VtankStorage.IsAvailable || VtankStorage.ReadText(fileName) is not null)
|
||||||
foreach (char value in name.Trim())
|
|
||||||
{
|
{
|
||||||
result.Append(value is '/' or '\\' || invalid.Contains(value)
|
_pendingLegacyBareName = null;
|
||||||
? '_'
|
return;
|
||||||
: value);
|
|
||||||
}
|
}
|
||||||
return result.Length == 0 ? "Meta" : result.ToString();
|
string legacyKey = _pendingLegacyBareName is { Length: > 0 } bareName
|
||||||
|
? LegacyNamedKey(bareName)
|
||||||
|
: LegacyByCharacterKey();
|
||||||
|
MetaProfile? legacy = ReadLegacyJson(legacyKey);
|
||||||
|
if (legacy is null)
|
||||||
|
{
|
||||||
|
_pendingLegacyBareName = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!SaveTo(fileName, legacy, out string notice))
|
||||||
|
{
|
||||||
|
// Representational loss (a disabled rule) blocks the migration
|
||||||
|
// outright rather than silently dropping it: the legacy JSON
|
||||||
|
// stays put (still fully readable) until the user resolves it.
|
||||||
|
_host.Log.Warn($"MossTank could not migrate legacy Meta profile: {notice}");
|
||||||
|
_pendingLegacyBareName = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_host.Storage.IsAvailable)
|
||||||
|
_host.Storage.Delete(legacyKey);
|
||||||
|
_pendingLegacyBareName = null;
|
||||||
|
_host.Log.Warn($"Migrated legacy MossTank Meta profile '{legacyKey}' to '{fileName}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private T? Read<T>(string key)
|
private MetaProfile? ReadLegacyJson(string key)
|
||||||
{
|
{
|
||||||
if (!_host.Storage.IsAvailable)
|
if (!_host.Storage.IsAvailable)
|
||||||
return default;
|
return null;
|
||||||
string? json = null;
|
string? json = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
json = _host.Storage.ReadText(key);
|
json = _host.Storage.ReadText(key);
|
||||||
return string.IsNullOrWhiteSpace(json)
|
return string.IsNullOrWhiteSpace(json)
|
||||||
? default
|
? null
|
||||||
: JsonSerializer.Deserialize<T>(json, Options);
|
: JsonSerializer.Deserialize<MetaProfile>(json, JsonOptions);
|
||||||
}
|
}
|
||||||
catch (Exception error)
|
catch (Exception error)
|
||||||
{
|
{
|
||||||
RecoveryNotice = MossTankProfileRecovery.Preserve(
|
RecoveryNotice = MossTankProfileRecovery.Preserve(_host, "meta", key, json, error);
|
||||||
_host,
|
|
||||||
"meta",
|
|
||||||
key,
|
|
||||||
json,
|
|
||||||
error);
|
|
||||||
_host.Log.Error(RecoveryNotice, error);
|
_host.Log.Error(RecoveryNotice, error);
|
||||||
return default;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Write<T>(string key, T value)
|
private bool SaveTo(string fileName, MetaProfile profile, out string notice)
|
||||||
{
|
{
|
||||||
if (!_host.Storage.IsAvailable)
|
string text;
|
||||||
return;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_host.Storage.WriteText(key, JsonSerializer.Serialize(value, Options));
|
text = MetafSerializer.SaveMeta(profile);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException error)
|
||||||
|
{
|
||||||
|
notice = $"Meta profile '{fileName}' was NOT saved: {error.Message}";
|
||||||
|
SaveNotice = notice;
|
||||||
|
_host.Log.Warn(notice);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!VtankStorage.IsAvailable)
|
||||||
|
{
|
||||||
|
notice = "VTank Meta storage is unavailable.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
VtankStorage.WriteText(fileName, text);
|
||||||
}
|
}
|
||||||
catch (Exception error)
|
catch (Exception error)
|
||||||
{
|
{
|
||||||
_host.Log.Error($"Unable to save MossTank Meta profile '{key}'.", error);
|
notice = $"Meta profile '{fileName}' could not be saved: {error.Message}";
|
||||||
|
SaveNotice = notice;
|
||||||
|
_host.Log.Warn(notice);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
SaveNotice = null;
|
||||||
|
notice = string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// File naming, storage plumbing.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
private string CurrentFileName() => _selected.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? VtankProfileDirectory.AutoCharacterFileName(_character, Server, "af")
|
||||||
|
: _selected;
|
||||||
|
|
||||||
|
private void WriteBinding()
|
||||||
|
{
|
||||||
|
if (!CanBindFiles || !VtankStorage.IsAvailable)
|
||||||
|
return;
|
||||||
|
VtankProfileDirectory.VtankCharacterBinding existing =
|
||||||
|
VtankProfileDirectory.TryReadCharacterBinding(VtankStorage, _character, Server)
|
||||||
|
?? new VtankProfileDirectory.VtankCharacterBinding(
|
||||||
|
string.Empty, string.Empty, string.Empty, CurrentFileName());
|
||||||
|
VtankProfileDirectory.WriteCharacterBinding(
|
||||||
|
VtankStorage,
|
||||||
|
_character,
|
||||||
|
Server,
|
||||||
|
existing with { MetaFileName = CurrentFileName() });
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MetaProfile Clone(MetaProfile profile) =>
|
private static MetaProfile Clone(MetaProfile profile) =>
|
||||||
JsonSerializer.Deserialize<MetaProfile>(
|
JsonSerializer.Deserialize<MetaProfile>(
|
||||||
JsonSerializer.Serialize(profile, Options),
|
JsonSerializer.Serialize(profile, JsonOptions),
|
||||||
Options) ?? new MetaProfile();
|
JsonOptions) ?? new MetaProfile();
|
||||||
|
|
||||||
private static string Normalize(string? name) => name?.Trim() ?? string.Empty;
|
private static string Normalize(string? name) => name?.Trim() ?? string.Empty;
|
||||||
|
|
||||||
|
private string LegacyByCharacterKey() =>
|
||||||
|
$"profiles/meta/by-character/{Hash(_character)}.json";
|
||||||
|
|
||||||
|
private static string LegacyNamedKey(string name) =>
|
||||||
|
$"profiles/meta/named/{Hash(name)}.json";
|
||||||
|
|
||||||
private static string Hash(string value)
|
private static string Hash(string value)
|
||||||
{
|
{
|
||||||
byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(value.ToLowerInvariant()));
|
byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(value.ToLowerInvariant()));
|
||||||
return Convert.ToHexString(hash.AsSpan(0, 12)).ToLowerInvariant();
|
return Convert.ToHexString(hash.AsSpan(0, 12)).ToLowerInvariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class IndexDocument
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
{
|
{
|
||||||
public List<string>? Names { get; set; } = [];
|
WriteIndented = true,
|
||||||
public Dictionary<string, string>? SelectedByCharacter { get; set; } = [];
|
PropertyNameCaseInsensitive = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read-only: the pre-cutover roster shape at LegacyRosterKey, kept
|
||||||
|
// solely so SweepLegacyRosterIfNeeded can recover it once.
|
||||||
|
private sealed class LegacyRosterDocument
|
||||||
|
{
|
||||||
|
public List<string> Names { get; set; } = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,14 @@ internal sealed partial class MossTankPanel
|
||||||
private readonly CombatSettings _combatSettings = new();
|
private readonly CombatSettings _combatSettings = new();
|
||||||
private readonly InventorySettings _inventorySettings = new();
|
private readonly InventorySettings _inventorySettings = new();
|
||||||
private readonly NavigationSettings _navigationSettings = new();
|
private readonly NavigationSettings _navigationSettings = new();
|
||||||
|
/// <summary>
|
||||||
|
/// One stable bundle of every live settings object the VTank-catalog
|
||||||
|
/// Settings profile (<c>.usd</c>) covers, built once so
|
||||||
|
/// <see cref="MossTankProfileStore"/>'s Load/Save/Create/ClearCurrent/
|
||||||
|
/// SetOptionInAll surface never has to repeat five constructor
|
||||||
|
/// parameters at every call site.
|
||||||
|
/// </summary>
|
||||||
|
private readonly VtankSettingsProfileSerializer.AllSettings _allSettings;
|
||||||
private readonly MossTankProfileStore _profiles;
|
private readonly MossTankProfileStore _profiles;
|
||||||
private readonly MossTankLootProfileStore _lootProfiles;
|
private readonly MossTankLootProfileStore _lootProfiles;
|
||||||
private readonly MossTankRouteProfileStore _routeProfiles;
|
private readonly MossTankRouteProfileStore _routeProfiles;
|
||||||
|
|
@ -182,14 +190,17 @@ internal sealed partial class MossTankPanel
|
||||||
{
|
{
|
||||||
_host = host;
|
_host = host;
|
||||||
_firstRunGuidancePending = NeedsFirstRunGuidance(host);
|
_firstRunGuidancePending = NeedsFirstRunGuidance(host);
|
||||||
|
_allSettings = new VtankSettingsProfileSerializer.AllSettings
|
||||||
|
{
|
||||||
|
Combat = _combatSettings,
|
||||||
|
Buffs = _buffSettings,
|
||||||
|
Vitals = _vitalSettings,
|
||||||
|
Inventory = _inventorySettings,
|
||||||
|
Navigation = _navigationSettings,
|
||||||
|
};
|
||||||
_profiles = new MossTankProfileStore(host);
|
_profiles = new MossTankProfileStore(host);
|
||||||
_profiles.BindCharacter(host.Automation.Character.Name);
|
_profiles.BindCharacter(host.Automation.Character.Name);
|
||||||
_profiles.LoadCurrent(
|
_profiles.LoadCurrent(_allSettings, _noBuffItemNames);
|
||||||
_combatSettings,
|
|
||||||
_buffSettings,
|
|
||||||
_vitalSettings,
|
|
||||||
_inventorySettings,
|
|
||||||
_noBuffItemNames);
|
|
||||||
_lootProfiles = new MossTankLootProfileStore(host);
|
_lootProfiles = new MossTankLootProfileStore(host);
|
||||||
_lootProfiles.BindCharacter(host.Automation.Character.Name);
|
_lootProfiles.BindCharacter(host.Automation.Character.Name);
|
||||||
if (!_lootProfiles.LoadCurrent(
|
if (!_lootProfiles.LoadCurrent(
|
||||||
|
|
@ -202,7 +213,7 @@ internal sealed partial class MossTankPanel
|
||||||
}
|
}
|
||||||
_routeProfiles = new MossTankRouteProfileStore(host);
|
_routeProfiles = new MossTankRouteProfileStore(host);
|
||||||
_routeProfiles.BindCharacter(host.Automation.Character.Name);
|
_routeProfiles.BindCharacter(host.Automation.Character.Name);
|
||||||
if (!_routeProfiles.LoadCurrent(_navigationSettings))
|
if (!_routeProfiles.LoadCurrent(_navigationSettings, host.Automation.Spells))
|
||||||
_routeProfiles.SaveCurrent(_navigationSettings);
|
_routeProfiles.SaveCurrent(_navigationSettings);
|
||||||
_combat = new CombatController(host, _combatSettings, _vitalSettings);
|
_combat = new CombatController(host, _combatSettings, _vitalSettings);
|
||||||
_buffCasterPreparer = new BuffCasterPreparer(
|
_buffCasterPreparer = new BuffCasterPreparer(
|
||||||
|
|
@ -583,6 +594,7 @@ internal sealed partial class MossTankPanel
|
||||||
public Action CopyLootProfile => () =>
|
public Action CopyLootProfile => () =>
|
||||||
CreateLootProfileCore(copyCurrent: true);
|
CreateLootProfileCore(copyCurrent: true);
|
||||||
public Action ClearLootProfile => ClearLootProfileCore;
|
public Action ClearLootProfile => ClearLootProfileCore;
|
||||||
|
public Action DeleteLootProfile => DeleteLootProfileCore;
|
||||||
public Action CloseLootEditor => () => _lootEditorVisible = false;
|
public Action CloseLootEditor => () => _lootEditorVisible = false;
|
||||||
public Action<int> SelectLootRule => SelectLootRuleCore;
|
public Action<int> SelectLootRule => SelectLootRuleCore;
|
||||||
public Action<string> SetLootExpressionDraft => value =>
|
public Action<string> SetLootExpressionDraft => value =>
|
||||||
|
|
@ -745,6 +757,7 @@ internal sealed partial class MossTankPanel
|
||||||
public Action CopyRouteProfile => () =>
|
public Action CopyRouteProfile => () =>
|
||||||
CreateRouteProfileCore(copyCurrent: true);
|
CreateRouteProfileCore(copyCurrent: true);
|
||||||
public Action ClearRouteProfile => ClearRouteProfileCore;
|
public Action ClearRouteProfile => ClearRouteProfileCore;
|
||||||
|
public Action DeleteRouteProfile => DeleteRouteProfileCore;
|
||||||
public Action SetFollowTarget => CaptureFollowTarget;
|
public Action SetFollowTarget => CaptureFollowTarget;
|
||||||
|
|
||||||
// ── Meta profile / editor ────────────────────────────────────────────
|
// ── Meta profile / editor ────────────────────────────────────────────
|
||||||
|
|
@ -815,6 +828,7 @@ internal sealed partial class MossTankPanel
|
||||||
public Action CreateMetaProfile => () => CreateMetaProfileCore(copyCurrent: false);
|
public Action CreateMetaProfile => () => CreateMetaProfileCore(copyCurrent: false);
|
||||||
public Action CopyMetaProfile => () => CreateMetaProfileCore(copyCurrent: true);
|
public Action CopyMetaProfile => () => CreateMetaProfileCore(copyCurrent: true);
|
||||||
public Action ClearMetaProfile => ClearMetaProfileCore;
|
public Action ClearMetaProfile => ClearMetaProfileCore;
|
||||||
|
public Action DeleteMetaProfile => DeleteMetaProfileCore;
|
||||||
|
|
||||||
// ── Profiles tab ─────────────────────────────────────────────────────
|
// ── Profiles tab ─────────────────────────────────────────────────────
|
||||||
public IReadOnlyList<string> MacroProfileNames => _profiles.AvailableNames;
|
public IReadOnlyList<string> MacroProfileNames => _profiles.AvailableNames;
|
||||||
|
|
@ -839,6 +853,7 @@ internal sealed partial class MossTankPanel
|
||||||
public Action CreateProfile => () => CreateProfileCore(copyCurrent: false);
|
public Action CreateProfile => () => CreateProfileCore(copyCurrent: false);
|
||||||
public Action CopyProfile => () => CreateProfileCore(copyCurrent: true);
|
public Action CopyProfile => () => CreateProfileCore(copyCurrent: true);
|
||||||
public Action ClearProfile => ClearProfileCore;
|
public Action ClearProfile => ClearProfileCore;
|
||||||
|
public Action DeleteProfile => DeleteProfileCore;
|
||||||
public Action ToggleMineOnly => () =>
|
public Action ToggleMineOnly => () =>
|
||||||
{
|
{
|
||||||
string before = _profiles.Selected;
|
string before = _profiles.Selected;
|
||||||
|
|
@ -1413,6 +1428,17 @@ internal sealed partial class MossTankPanel
|
||||||
_lootEditorNotice = $"Cleared {_lootProfiles.Selected}.";
|
_lootEditorNotice = $"Cleared {_lootProfiles.Selected}.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DeleteLootProfileCore()
|
||||||
|
{
|
||||||
|
if (!_lootProfiles.Delete(out string notice))
|
||||||
|
{
|
||||||
|
_lootEditorNotice = notice;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LoadLootProfile();
|
||||||
|
_lootEditorNotice = notice;
|
||||||
|
}
|
||||||
|
|
||||||
private void LoadLootProfile()
|
private void LoadLootProfile()
|
||||||
{
|
{
|
||||||
if (!_lootProfiles.LoadCurrent(
|
if (!_lootProfiles.LoadCurrent(
|
||||||
|
|
@ -1587,7 +1613,15 @@ internal sealed partial class MossTankPanel
|
||||||
AddRouteWaypoint(new RouteWaypoint
|
AddRouteWaypoint(new RouteWaypoint
|
||||||
{
|
{
|
||||||
Type = type,
|
Type = type,
|
||||||
Position = target.Position,
|
// Retail's own header x/y/z for Portal2/UseNPC is dead weight —
|
||||||
|
// e9/fa extend `at`, whose position accessor is the player's
|
||||||
|
// OWN live position, never read back into anything meaningful
|
||||||
|
// (docs/research/vtank-kb/06-navigation-and-nav.md section 1.2).
|
||||||
|
// ReferencePosition is the embedded d-record: the real target
|
||||||
|
// coordinate matched against live world objects by name+class+
|
||||||
|
// proximity (Navigation.cs's TickUse).
|
||||||
|
Position = _host.Automation.Navigation.Snapshot.Position,
|
||||||
|
ReferencePosition = target.Position,
|
||||||
ObjectId = target.ObjectId,
|
ObjectId = target.ObjectId,
|
||||||
ObjectName = target.Name,
|
ObjectName = target.Name,
|
||||||
});
|
});
|
||||||
|
|
@ -1764,9 +1798,20 @@ internal sealed partial class MossTankPanel
|
||||||
_routeNotice = $"Cleared {_routeProfiles.Selected}.";
|
_routeNotice = $"Cleared {_routeProfiles.Selected}.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DeleteRouteProfileCore()
|
||||||
|
{
|
||||||
|
if (!_routeProfiles.Delete(out string notice))
|
||||||
|
{
|
||||||
|
_routeNotice = notice;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LoadRouteProfile();
|
||||||
|
_routeNotice = notice;
|
||||||
|
}
|
||||||
|
|
||||||
private void LoadRouteProfile()
|
private void LoadRouteProfile()
|
||||||
{
|
{
|
||||||
if (!_routeProfiles.LoadCurrent(_navigationSettings))
|
if (!_routeProfiles.LoadCurrent(_navigationSettings, _host.Automation.Spells))
|
||||||
_routeProfiles.SaveCurrent(_navigationSettings);
|
_routeProfiles.SaveCurrent(_navigationSettings);
|
||||||
if (_initialized)
|
if (_initialized)
|
||||||
ApplyPersistedOptionOverrides();
|
ApplyPersistedOptionOverrides();
|
||||||
|
|
@ -2225,9 +2270,9 @@ internal sealed partial class MossTankPanel
|
||||||
}
|
}
|
||||||
_metaProfile.Rules.Add(rule);
|
_metaProfile.Rules.Add(rule);
|
||||||
_selectedMetaRule = _metaProfile.Rules.Count - 1;
|
_selectedMetaRule = _metaProfile.Rules.Count - 1;
|
||||||
SaveMetaProfile();
|
|
||||||
RefreshMetaEditor();
|
RefreshMetaEditor();
|
||||||
_metaNotice = $"Added rule in {rule.State}.";
|
if (SaveMetaProfile())
|
||||||
|
_metaNotice = $"Added rule in {rule.State}.";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyMetaRuleCore()
|
private void ApplyMetaRuleCore()
|
||||||
|
|
@ -2244,9 +2289,9 @@ internal sealed partial class MossTankPanel
|
||||||
}
|
}
|
||||||
replacement.Id = current.Id;
|
replacement.Id = current.Id;
|
||||||
_metaProfile.Rules[_selectedMetaRule] = replacement;
|
_metaProfile.Rules[_selectedMetaRule] = replacement;
|
||||||
SaveMetaProfile();
|
|
||||||
RefreshMetaEditor();
|
RefreshMetaEditor();
|
||||||
_metaNotice = $"Updated rule in {replacement.State}.";
|
if (SaveMetaProfile())
|
||||||
|
_metaNotice = $"Updated rule in {replacement.State}.";
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryBuildMetaRule(out MetaRule rule, out string error)
|
private bool TryBuildMetaRule(out MetaRule rule, out string error)
|
||||||
|
|
@ -2301,9 +2346,9 @@ internal sealed partial class MossTankPanel
|
||||||
_selectedMetaRule = Math.Min(
|
_selectedMetaRule = Math.Min(
|
||||||
_selectedMetaRule,
|
_selectedMetaRule,
|
||||||
Math.Max(0, _metaProfile.Rules.Count - 1));
|
Math.Max(0, _metaProfile.Rules.Count - 1));
|
||||||
SaveMetaProfile();
|
|
||||||
RefreshMetaEditor();
|
RefreshMetaEditor();
|
||||||
_metaNotice = $"Removed rule from {selected.State}.";
|
if (SaveMetaProfile())
|
||||||
|
_metaNotice = $"Removed rule from {selected.State}.";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MoveMetaRule(int direction)
|
private void MoveMetaRule(int direction)
|
||||||
|
|
@ -2318,9 +2363,9 @@ internal sealed partial class MossTankPanel
|
||||||
_metaProfile.Rules.RemoveAt(_selectedMetaRule);
|
_metaProfile.Rules.RemoveAt(_selectedMetaRule);
|
||||||
_metaProfile.Rules.Insert(destination, rule);
|
_metaProfile.Rules.Insert(destination, rule);
|
||||||
_selectedMetaRule = destination;
|
_selectedMetaRule = destination;
|
||||||
SaveMetaProfile();
|
|
||||||
RefreshMetaEditor();
|
RefreshMetaEditor();
|
||||||
_metaNotice = $"Moved {rule.State} rule.";
|
if (SaveMetaProfile())
|
||||||
|
_metaNotice = $"Moved {rule.State} rule.";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SelectMetaProfileCore(string name)
|
private void SelectMetaProfileCore(string name)
|
||||||
|
|
@ -2360,6 +2405,17 @@ internal sealed partial class MossTankPanel
|
||||||
_metaNotice = $"Cleared Meta profile {_metaProfiles.Selected}.";
|
_metaNotice = $"Cleared Meta profile {_metaProfiles.Selected}.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DeleteMetaProfileCore()
|
||||||
|
{
|
||||||
|
if (!_metaProfiles.Delete(out string notice))
|
||||||
|
{
|
||||||
|
_metaNotice = notice;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LoadMetaProfile();
|
||||||
|
_metaNotice = notice;
|
||||||
|
}
|
||||||
|
|
||||||
private void LoadMetaProfile()
|
private void LoadMetaProfile()
|
||||||
{
|
{
|
||||||
_metaProfile = _metaProfiles.LoadCurrent();
|
_metaProfile = _metaProfiles.LoadCurrent();
|
||||||
|
|
@ -2370,7 +2426,22 @@ internal sealed partial class MossTankPanel
|
||||||
RefreshMetaEditor();
|
RefreshMetaEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveMetaProfile() => _metaProfiles.SaveCurrent(_metaProfile);
|
/// <summary>
|
||||||
|
/// Saves the current Meta profile as <c>.af</c>. Returns
|
||||||
|
/// <see langword="false"/> when the save was refused (a disabled rule
|
||||||
|
/// has no metaf-compatible representation) — callers must not overwrite
|
||||||
|
/// <see cref="_metaNotice"/> with a generic success message in that
|
||||||
|
/// case, since <see cref="MossTankMetaProfileStore.SaveNotice"/> already
|
||||||
|
/// carries the user-visible reason.
|
||||||
|
/// </summary>
|
||||||
|
private bool SaveMetaProfile()
|
||||||
|
{
|
||||||
|
if (_metaProfiles.SaveCurrent(_metaProfile))
|
||||||
|
return true;
|
||||||
|
if (_metaProfiles.SaveNotice is { } notice)
|
||||||
|
_metaNotice = notice;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static string DescribeMetaCondition(MetaCondition condition) =>
|
private static string DescribeMetaCondition(MetaCondition condition) =>
|
||||||
condition.Kind switch
|
condition.Kind switch
|
||||||
|
|
@ -2408,19 +2479,16 @@ internal sealed partial class MossTankPanel
|
||||||
return nearest;
|
return nearest;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadEmbeddedNavigationRoute(string source)
|
private void LoadEmbeddedNavigationRoute(NavigationSettings? route)
|
||||||
{
|
{
|
||||||
_navigation.Reset();
|
_navigation.Reset();
|
||||||
if (!VtankNavRouteSerializer.TryLoad(
|
if (route is null)
|
||||||
source,
|
|
||||||
_navigationSettings,
|
|
||||||
_host.Automation.Spells,
|
|
||||||
out string error))
|
|
||||||
{
|
{
|
||||||
_routeNotice = $"Embedded route rejected: {error}";
|
_routeNotice = "Embedded route rejected: unresolved Nav tag.";
|
||||||
_host.Log.Warn($"MossTank Meta embedded route rejected: {error}");
|
_host.Log.Warn("MossTank Meta embedded route rejected: unresolved Nav tag.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
VtankNavRouteSerializer.Apply(route, _navigationSettings);
|
||||||
_routeProfiles.SaveCurrent(_navigationSettings);
|
_routeProfiles.SaveCurrent(_navigationSettings);
|
||||||
_selectedRouteWaypoint = 0;
|
_selectedRouteWaypoint = 0;
|
||||||
RefreshRouteEditor();
|
RefreshRouteEditor();
|
||||||
|
|
@ -2528,7 +2596,7 @@ internal sealed partial class MossTankPanel
|
||||||
_combatSettings.DebuffPrecastSeconds),
|
_combatSettings.DebuffPrecastSeconds),
|
||||||
"switchwandstodebuff" => ExpressionValue.Boolean(
|
"switchwandstodebuff" => ExpressionValue.Boolean(
|
||||||
_combatSettings.SwitchWandsToDebuff),
|
_combatSettings.SwitchWandsToDebuff),
|
||||||
"usearcs" => ExpressionValue.Boolean(_combatSettings.UseArcs),
|
"usearcs" => ExpressionValue.Number((int)_combatSettings.UseArcs),
|
||||||
"deleteghostmonsters" => ExpressionValue.Boolean(
|
"deleteghostmonsters" => ExpressionValue.Boolean(
|
||||||
_combatSettings.DeleteGhostMonsters),
|
_combatSettings.DeleteGhostMonsters),
|
||||||
"ghostmonsterspellattemptcount" => ExpressionValue.Number(
|
"ghostmonsterspellattemptcount" => ExpressionValue.Number(
|
||||||
|
|
@ -2998,7 +3066,8 @@ internal sealed partial class MossTankPanel
|
||||||
_combatSettings.SwitchWandsToDebuff = value.IsTruthy;
|
_combatSettings.SwitchWandsToDebuff = value.IsTruthy;
|
||||||
break;
|
break;
|
||||||
case "usearcs":
|
case "usearcs":
|
||||||
_combatSettings.UseArcs = value.IsTruthy;
|
_combatSettings.UseArcs = (UseArcsMode)Math.Clamp(
|
||||||
|
value.AsInt32("UseArcs"), 1, 3);
|
||||||
break;
|
break;
|
||||||
case "deleteghostmonsters":
|
case "deleteghostmonsters":
|
||||||
_combatSettings.DeleteGhostMonsters = value.IsTruthy;
|
_combatSettings.DeleteGhostMonsters = value.IsTruthy;
|
||||||
|
|
@ -3414,10 +3483,7 @@ internal sealed partial class MossTankPanel
|
||||||
if (!_profiles.Create(
|
if (!_profiles.Create(
|
||||||
_profileNameDraft,
|
_profileNameDraft,
|
||||||
copyCurrent,
|
copyCurrent,
|
||||||
_combatSettings,
|
_allSettings,
|
||||||
_buffSettings,
|
|
||||||
_vitalSettings,
|
|
||||||
_inventorySettings,
|
|
||||||
_noBuffItemNames,
|
_noBuffItemNames,
|
||||||
out string notice))
|
out string notice))
|
||||||
{
|
{
|
||||||
|
|
@ -3431,24 +3497,25 @@ internal sealed partial class MossTankPanel
|
||||||
|
|
||||||
private void ClearProfileCore()
|
private void ClearProfileCore()
|
||||||
{
|
{
|
||||||
_profiles.ClearCurrent(
|
_profiles.ClearCurrent(_allSettings, _noBuffItemNames);
|
||||||
_combatSettings,
|
|
||||||
_buffSettings,
|
|
||||||
_vitalSettings,
|
|
||||||
_inventorySettings,
|
|
||||||
_noBuffItemNames);
|
|
||||||
_profileLifecycleNotice = $"Cleared {_profiles.Selected} to VTank defaults.";
|
_profileLifecycleNotice = $"Cleared {_profiles.Selected} to VTank defaults.";
|
||||||
ResetProfileConsumers();
|
ResetProfileConsumers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DeleteProfileCore()
|
||||||
|
{
|
||||||
|
if (!_profiles.Delete(out string notice))
|
||||||
|
{
|
||||||
|
_profileLifecycleNotice = notice;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LoadSelectedProfile();
|
||||||
|
_profileLifecycleNotice = notice;
|
||||||
|
}
|
||||||
|
|
||||||
private void LoadSelectedProfile()
|
private void LoadSelectedProfile()
|
||||||
{
|
{
|
||||||
_profiles.LoadCurrent(
|
_profiles.LoadCurrent(_allSettings, _noBuffItemNames);
|
||||||
_combatSettings,
|
|
||||||
_buffSettings,
|
|
||||||
_vitalSettings,
|
|
||||||
_inventorySettings,
|
|
||||||
_noBuffItemNames);
|
|
||||||
LoadLootProfile();
|
LoadLootProfile();
|
||||||
LoadRouteProfile();
|
LoadRouteProfile();
|
||||||
ApplyPersistedOptionOverrides();
|
ApplyPersistedOptionOverrides();
|
||||||
|
|
@ -3532,12 +3599,7 @@ internal sealed partial class MossTankPanel
|
||||||
|
|
||||||
private void SaveProfile()
|
private void SaveProfile()
|
||||||
{
|
{
|
||||||
_profiles.SaveCurrent(
|
_profiles.SaveCurrent(_allSettings, _noBuffItemNames);
|
||||||
_combatSettings,
|
|
||||||
_buffSettings,
|
|
||||||
_vitalSettings,
|
|
||||||
_inventorySettings,
|
|
||||||
_noBuffItemNames);
|
|
||||||
_lootProfiles.SaveCurrent(
|
_lootProfiles.SaveCurrent(
|
||||||
_inventorySettings.Loot.Rules,
|
_inventorySettings.Loot.Rules,
|
||||||
_inventorySettings.Loot);
|
_inventorySettings.Loot);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -6,45 +6,86 @@ using AcDream.Plugin.Abstractions;
|
||||||
namespace AcDream.Plugins.MossTank;
|
namespace AcDream.Plugins.MossTank;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Independent VTank navigation-profile lifecycle. The selected route is
|
/// VTank-compatible navigation-route lifecycle. <c>.af</c> is the ONLY
|
||||||
/// remembered per character; "By char" is a private route document and named
|
/// storage/authoring format — but only for the fields metaf's own
|
||||||
/// profiles are reusable copies.
|
/// <c>NAV:</c> grammar actually represents (<see cref="NavigationSettings.Mode"/>,
|
||||||
|
/// <see cref="NavigationSettings.Waypoints"/>,
|
||||||
|
/// <see cref="NavigationSettings.FollowTargetObjectId"/>/
|
||||||
|
/// <see cref="NavigationSettings.FollowTargetName"/> — see
|
||||||
|
/// <see cref="MetafSerializer.SaveNav"/>/<see cref="MetafSerializer.TryLoadNav"/>).
|
||||||
|
/// Every other <see cref="NavigationSettings"/> field
|
||||||
|
/// (Enabled/Priority/MinimumDistanceMeters/FollowAroundCorners/OpenDoors/
|
||||||
|
/// Door*) is a real VTank Settings-table row with its own catalog name
|
||||||
|
/// (<c>EnableNav</c>, <c>NavPriorityBoost</c>, <c>NavCloseStopRange</c>,
|
||||||
|
/// …) already owned end-to-end by <see cref="MossTankProfileStore"/>'s
|
||||||
|
/// <c>.usd</c> profile — this store never touches them, matching real
|
||||||
|
/// VTank's own split between the Settings-scoped nav prefs and the
|
||||||
|
/// per-route file.
|
||||||
|
///
|
||||||
|
/// Named route files carry a <c>nav_</c> prefix (metaf's own observed
|
||||||
|
/// convention for a stand-alone nav-only <c>.af</c>, see the committed
|
||||||
|
/// <c>nav_*.af</c> fixtures) so a route and a Meta profile sharing the same
|
||||||
|
/// user-typed name never collide in the shared <see cref="IPluginHost.VtankProfiles"/>
|
||||||
|
/// directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class MossTankRouteProfileStore
|
internal sealed class MossTankRouteProfileStore
|
||||||
{
|
{
|
||||||
public const string ByCharacter = "By char";
|
public const string ByCharacter = "By char";
|
||||||
private const string IndexKey = "profiles/route/index.json";
|
private const string NavPrefix = "nav_";
|
||||||
private static readonly JsonSerializerOptions Options = new()
|
// The pre-cutover roster (round 3 item 2): before the .af cutover this
|
||||||
{
|
// store kept a flat Names list here (named routes were never
|
||||||
WriteIndented = true,
|
// owner-scoped — one shared, globally-hashed key per name). The
|
||||||
PropertyNameCaseInsensitive = true,
|
// cutover stopped reading this key at all, orphaning every named route
|
||||||
};
|
// except whichever one a character had selected (that one alone still
|
||||||
|
// converts, via MigrateLegacyIfNeeded). Read-only: consulted ONLY by
|
||||||
|
// the one-time SweepLegacyRosterIfNeeded.
|
||||||
|
private const string LegacyRosterKey = "profiles/route/index.json";
|
||||||
|
|
||||||
private readonly IPluginHost _host;
|
private readonly IPluginHost _host;
|
||||||
private IndexDocument _index;
|
|
||||||
private string _characterName = string.Empty;
|
private string _characterName = string.Empty;
|
||||||
private string _selected = ByCharacter;
|
private string _selected = ByCharacter;
|
||||||
|
private string? _pendingLegacyBareName;
|
||||||
|
private bool _rosterSwept;
|
||||||
|
|
||||||
public MossTankRouteProfileStore(IPluginHost host)
|
public MossTankRouteProfileStore(IPluginHost host)
|
||||||
{
|
{
|
||||||
_host = host ?? throw new ArgumentNullException(nameof(host));
|
_host = host ?? throw new ArgumentNullException(nameof(host));
|
||||||
_index = Read<IndexDocument>(IndexKey) ?? new IndexDocument();
|
|
||||||
_index.Names ??= [];
|
|
||||||
_index.SelectedByCharacter = new Dictionary<string, string>(
|
|
||||||
_index.SelectedByCharacter ?? new Dictionary<string, string>(),
|
|
||||||
StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Selected => _selected;
|
public string Selected => Strip(_selected);
|
||||||
public string? RecoveryNotice { get; private set; }
|
public string? RecoveryNotice { get; private set; }
|
||||||
public IReadOnlyList<string> AvailableNames => new[] { ByCharacter }
|
|
||||||
.Concat(_index.Names)
|
private string Server => _host.Automation.Character.WorldName;
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
private IPluginStorage VtankStorage => _host.VtankProfiles;
|
||||||
.OrderBy(name => name.Equals(
|
private bool CanBindFiles => _characterName.Length > 0 && Server.Length > 0;
|
||||||
ByCharacter,
|
|
||||||
StringComparison.OrdinalIgnoreCase) ? 0 : 1)
|
private static string Strip(string fileName)
|
||||||
.ThenBy(static name => name, StringComparer.OrdinalIgnoreCase)
|
{
|
||||||
.ToArray();
|
if (fileName.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return fileName;
|
||||||
|
string value = fileName;
|
||||||
|
if (value.StartsWith(NavPrefix, StringComparison.Ordinal))
|
||||||
|
value = value[NavPrefix.Length..];
|
||||||
|
if (value.EndsWith(".af", StringComparison.OrdinalIgnoreCase))
|
||||||
|
value = value[..^3];
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<string> AvailableNames
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var names = new List<string> { ByCharacter };
|
||||||
|
foreach (VtankProfileDirectory.ProfileEntry entry in
|
||||||
|
VtankProfileDirectory.ListNavigationProfiles(VtankStorage))
|
||||||
|
{
|
||||||
|
if (entry.FileName.Length == 0)
|
||||||
|
continue; // VTank's own "[None]"/"[By char]" sentinels.
|
||||||
|
names.Add(Strip(entry.FileName));
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool BindCharacter(string? characterName)
|
public bool BindCharacter(string? characterName)
|
||||||
{
|
{
|
||||||
|
|
@ -54,24 +95,46 @@ internal sealed class MossTankRouteProfileStore
|
||||||
if (string.Equals(normalized, _characterName, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(normalized, _characterName, StringComparison.OrdinalIgnoreCase))
|
||||||
return false;
|
return false;
|
||||||
_characterName = normalized;
|
_characterName = normalized;
|
||||||
_selected = _index.SelectedByCharacter.TryGetValue(
|
_pendingLegacyBareName = null;
|
||||||
SelectionKey(),
|
VtankProfileDirectory.VtankCharacterBinding? binding = CanBindFiles
|
||||||
out string? selected)
|
? VtankProfileDirectory.TryReadCharacterBinding(VtankStorage, _characterName, Server)
|
||||||
&& IsKnown(selected)
|
: null;
|
||||||
? CanonicalName(selected)
|
_selected = binding is { NavFileName.Length: > 0 } bound
|
||||||
: ByCharacter;
|
? bound.NavFileName
|
||||||
|
: ByCharacter;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Select(string? name)
|
public bool Select(string? name)
|
||||||
{
|
{
|
||||||
string normalized = name?.Trim() ?? string.Empty;
|
string normalized = Normalize(name);
|
||||||
if (!IsKnown(normalized))
|
if (normalized.Length == 0)
|
||||||
return false;
|
return false;
|
||||||
_selected = CanonicalName(normalized);
|
if (normalized.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase))
|
||||||
_index.SelectedByCharacter[SelectionKey()] = _selected;
|
{
|
||||||
SaveIndex();
|
_selected = ByCharacter;
|
||||||
return true;
|
_pendingLegacyBareName = null;
|
||||||
|
WriteBinding();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
string candidate = ToFileName(normalized);
|
||||||
|
if (VtankStorage.IsAvailable && VtankStorage.ReadText(candidate) is not null)
|
||||||
|
{
|
||||||
|
_selected = candidate;
|
||||||
|
_pendingLegacyBareName = null;
|
||||||
|
WriteBinding();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (_host.Storage.IsAvailable
|
||||||
|
&& _host.Storage.ReadText(LegacyProfileKey(normalized, byCharacter: false)) is not null)
|
||||||
|
{
|
||||||
|
_selected = candidate;
|
||||||
|
_pendingLegacyBareName = normalized;
|
||||||
|
WriteBinding();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Create(
|
public bool Create(
|
||||||
|
|
@ -80,7 +143,7 @@ internal sealed class MossTankRouteProfileStore
|
||||||
NavigationSettings current,
|
NavigationSettings current,
|
||||||
out string notice)
|
out string notice)
|
||||||
{
|
{
|
||||||
string normalized = name?.Trim() ?? string.Empty;
|
string normalized = Normalize(name);
|
||||||
if (normalized.Length is < 1 or > 64)
|
if (normalized.Length is < 1 or > 64)
|
||||||
{
|
{
|
||||||
notice = "Enter a route profile name (1-64 characters).";
|
notice = "Enter a route profile name (1-64 characters).";
|
||||||
|
|
@ -91,46 +154,40 @@ internal sealed class MossTankRouteProfileStore
|
||||||
notice = "'By char' is the built-in route profile.";
|
notice = "'By char' is the built-in route profile.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Write(
|
string fileName = ToFileName(normalized);
|
||||||
ProfileKey(normalized, byCharacter: false),
|
NavigationSettings source = copyCurrent ? current : new NavigationSettings();
|
||||||
copyCurrent
|
WriteAf(fileName, MetafSerializer.SaveNav(source));
|
||||||
? RouteDocument.Capture(current)
|
_selected = fileName;
|
||||||
: new RouteDocument());
|
_pendingLegacyBareName = null;
|
||||||
if (!_index.Names.Contains(normalized, StringComparer.OrdinalIgnoreCase))
|
WriteBinding();
|
||||||
_index.Names.Add(normalized);
|
|
||||||
_selected = _index.Names.First(entry => entry.Equals(
|
|
||||||
normalized,
|
|
||||||
StringComparison.OrdinalIgnoreCase));
|
|
||||||
_index.SelectedByCharacter[SelectionKey()] = _selected;
|
|
||||||
SaveIndex();
|
|
||||||
WriteLegacyExport(_selected, copyCurrent ? current : new NavigationSettings());
|
|
||||||
notice = copyCurrent
|
notice = copyCurrent
|
||||||
? $"Copied route to {_selected}."
|
? $"Copied route to {Strip(fileName)}."
|
||||||
: $"Created route profile {_selected}.";
|
: $"Created route profile {Strip(fileName)}.";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool LoadCurrent(NavigationSettings target)
|
public bool LoadCurrent(NavigationSettings target, ISpellCatalog spells)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(target);
|
ArgumentNullException.ThrowIfNull(target);
|
||||||
RouteDocument? document = Read<RouteDocument>(CurrentKey());
|
ArgumentNullException.ThrowIfNull(spells);
|
||||||
if (document is null)
|
SweepLegacyRosterIfNeeded();
|
||||||
|
MigrateLegacyIfNeeded(target, spells);
|
||||||
|
string fileName = CurrentFileName();
|
||||||
|
string? text = VtankStorage.IsAvailable ? VtankStorage.ReadText(fileName) : null;
|
||||||
|
if (text is null)
|
||||||
return false;
|
return false;
|
||||||
document.Apply(target);
|
if (!MetafSerializer.TryLoadNav(text, target, spells, out string error))
|
||||||
|
{
|
||||||
|
RecoveryNotice = MossTankProfileRecovery.Preserve(
|
||||||
|
_host, "route", fileName, text, new FormatException(error));
|
||||||
|
_host.Log.Warn(RecoveryNotice);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveCurrent(NavigationSettings settings)
|
public void SaveCurrent(NavigationSettings settings) =>
|
||||||
{
|
WriteAf(CurrentFileName(), MetafSerializer.SaveNav(settings));
|
||||||
Write(CurrentKey(), RouteDocument.Capture(settings));
|
|
||||||
WriteLegacyExport(
|
|
||||||
_selected.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase)
|
|
||||||
? string.IsNullOrWhiteSpace(_characterName)
|
|
||||||
? ByCharacter
|
|
||||||
: _characterName
|
|
||||||
: _selected,
|
|
||||||
settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryImportLegacy(
|
public bool TryImportLegacy(
|
||||||
string? name,
|
string? name,
|
||||||
|
|
@ -138,7 +195,7 @@ internal sealed class MossTankRouteProfileStore
|
||||||
ISpellCatalog spells,
|
ISpellCatalog spells,
|
||||||
out string notice)
|
out string notice)
|
||||||
{
|
{
|
||||||
string normalized = name?.Trim() ?? string.Empty;
|
string normalized = Normalize(name);
|
||||||
if (!_host.Storage.IsAvailable || normalized.Length == 0)
|
if (!_host.Storage.IsAvailable || normalized.Length == 0)
|
||||||
{
|
{
|
||||||
notice = "Legacy navigation storage is unavailable.";
|
notice = "Legacy navigation storage is unavailable.";
|
||||||
|
|
@ -162,64 +219,163 @@ internal sealed class MossTankRouteProfileStore
|
||||||
notice = $"Could not import {Path.GetFileName(key)}: {error}";
|
notice = $"Could not import {Path.GetFileName(key)}: {error}";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!_index.Names.Contains(normalized, StringComparer.OrdinalIgnoreCase))
|
string fileName = ToFileName(normalized);
|
||||||
_index.Names.Add(normalized);
|
WriteAf(fileName, MetafSerializer.SaveNav(target));
|
||||||
_selected = _index.Names.First(entry => entry.Equals(
|
_selected = fileName;
|
||||||
normalized,
|
_pendingLegacyBareName = null;
|
||||||
StringComparison.OrdinalIgnoreCase));
|
WriteBinding();
|
||||||
_index.SelectedByCharacter[SelectionKey()] = _selected;
|
notice = $"Imported VTank navigation profile {Strip(fileName)}.";
|
||||||
SaveIndex();
|
|
||||||
SaveCurrent(target);
|
|
||||||
notice = $"Imported VTank navigation profile {_selected}.";
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round 3 item 10: deletes the currently selected named route's real
|
||||||
|
/// <c>.af</c> file, then falls back to <see cref="ByCharacter"/>.
|
||||||
|
/// Refuses for <see cref="ByCharacter"/> itself — there is no file to
|
||||||
|
/// delete, only a reset (see <see cref="ClearCurrent"/>), matching
|
||||||
|
/// <see cref="MossTankProfileStore.Delete"/>'s own contract.
|
||||||
|
/// </summary>
|
||||||
|
public bool Delete(out string notice)
|
||||||
|
{
|
||||||
|
if (_selected.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
notice = "'By char' is the built-in route profile and cannot be deleted.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string fileName = _selected;
|
||||||
|
if (VtankStorage.IsAvailable)
|
||||||
|
VtankStorage.Delete(fileName);
|
||||||
|
_selected = ByCharacter;
|
||||||
|
_pendingLegacyBareName = null;
|
||||||
|
WriteBinding();
|
||||||
|
notice = $"Deleted route profile {Strip(fileName)}.";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets only the fields this store owns (Mode/Waypoints/FollowTarget)
|
||||||
|
/// — Enabled/Priority/MinimumDistanceMeters/FollowAroundCorners/
|
||||||
|
/// OpenDoors/Door* belong to the Settings profile and are left alone.
|
||||||
|
/// </summary>
|
||||||
public void ClearCurrent(NavigationSettings target)
|
public void ClearCurrent(NavigationSettings target)
|
||||||
{
|
{
|
||||||
target.Enabled = false;
|
|
||||||
target.Priority = false;
|
|
||||||
target.Mode = RouteMode.Circular;
|
target.Mode = RouteMode.Circular;
|
||||||
target.MinimumDistanceMeters = 2d;
|
|
||||||
target.FollowTargetObjectId = 0u;
|
target.FollowTargetObjectId = 0u;
|
||||||
target.FollowTargetName = string.Empty;
|
target.FollowTargetName = string.Empty;
|
||||||
target.FollowAroundCorners = true;
|
|
||||||
target.OpenDoors = false;
|
|
||||||
target.Waypoints.Clear();
|
target.Waypoints.Clear();
|
||||||
SaveCurrent(target);
|
SaveCurrent(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsKnown(string? name) => name is not null
|
// ------------------------------------------------------------------
|
||||||
&& (name.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase)
|
// Legacy JSON -> .af migration.
|
||||||
|| _index.Names.Contains(name, StringComparer.OrdinalIgnoreCase));
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
private string CanonicalName(string name) => name.Equals(
|
/// <summary>
|
||||||
ByCharacter,
|
/// Round 3 item 2: a ONE-TIME sweep (guarded by <see cref="_rosterSwept"/>)
|
||||||
StringComparison.OrdinalIgnoreCase)
|
/// over the pre-cutover roster at <see cref="LegacyRosterKey"/>. Unlike
|
||||||
? ByCharacter
|
/// <see cref="MigrateLegacyIfNeeded"/> — which only ever converts the
|
||||||
: _index.Names.First(entry => entry.Equals(
|
/// ONE currently-selected route — this converts every OTHER named route
|
||||||
name,
|
/// the old roster still lists, using a fresh <see cref="NavigationSettings"/>
|
||||||
StringComparison.OrdinalIgnoreCase));
|
/// per entry so the live/selected route is never touched. Named routes
|
||||||
|
/// were never owner-scoped (one shared, globally-hashed key per name),
|
||||||
private string CurrentKey() => _selected.Equals(
|
/// so every entry converts regardless of which character is currently
|
||||||
ByCharacter,
|
/// bound.
|
||||||
StringComparison.OrdinalIgnoreCase)
|
/// </summary>
|
||||||
? ProfileKey(_characterName, byCharacter: true)
|
private void SweepLegacyRosterIfNeeded()
|
||||||
: ProfileKey(_selected, byCharacter: false);
|
|
||||||
|
|
||||||
private static string ProfileKey(string value, bool byCharacter)
|
|
||||||
{
|
{
|
||||||
string identity = (byCharacter ? "char:" : "named:")
|
if (_rosterSwept)
|
||||||
+ value.Trim().ToUpperInvariant();
|
return;
|
||||||
string hash = Convert.ToHexString(
|
_rosterSwept = true;
|
||||||
SHA256.HashData(Encoding.UTF8.GetBytes(identity)));
|
if (!_host.Storage.IsAvailable)
|
||||||
return $"profiles/route/{hash}.json";
|
return;
|
||||||
|
List<string>? names = ReadRosterNames();
|
||||||
|
if (names is not { Count: > 0 })
|
||||||
|
return;
|
||||||
|
|
||||||
|
var remaining = new List<string>();
|
||||||
|
int migrated = 0;
|
||||||
|
foreach (string rawName in names)
|
||||||
|
{
|
||||||
|
string name = (rawName ?? string.Empty).Trim();
|
||||||
|
if (name.Length == 0 || name.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
continue; // stale/invalid row; drop it rather than loop on it forever.
|
||||||
|
|
||||||
|
string legacyKey = LegacyProfileKey(name, byCharacter: false);
|
||||||
|
LegacyRouteDocument? legacy = ReadLegacyJson(legacyKey);
|
||||||
|
if (legacy is null)
|
||||||
|
continue; // already converted (or never existed); drop the row.
|
||||||
|
|
||||||
|
string fileName = ToFileName(name);
|
||||||
|
if (VtankStorage.IsAvailable && VtankStorage.ReadText(fileName) is null)
|
||||||
|
{
|
||||||
|
var scratch = new NavigationSettings();
|
||||||
|
legacy.ApplyRouteOnly(scratch);
|
||||||
|
WriteAf(fileName, MetafSerializer.SaveNav(scratch));
|
||||||
|
}
|
||||||
|
_host.Storage.Delete(legacyKey);
|
||||||
|
migrated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (migrated == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (remaining.Count > 0)
|
||||||
|
{
|
||||||
|
_host.Storage.WriteText(
|
||||||
|
LegacyRosterKey,
|
||||||
|
JsonSerializer.Serialize(new LegacyRosterDocument { Names = remaining }, JsonOptions));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_host.Storage.Delete(LegacyRosterKey);
|
||||||
|
}
|
||||||
|
_host.Log.Warn($"Migrated {migrated} legacy MossTank named route profile(s) from the old roster.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private string SelectionKey() => string.IsNullOrWhiteSpace(_characterName)
|
private List<string>? ReadRosterNames()
|
||||||
? "_default"
|
{
|
||||||
: _characterName;
|
string? json = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
json = _host.Storage.ReadText(LegacyRosterKey);
|
||||||
|
return string.IsNullOrWhiteSpace(json)
|
||||||
|
? null
|
||||||
|
: JsonSerializer.Deserialize<LegacyRosterDocument>(json, JsonOptions)?.Names;
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
RecoveryNotice = MossTankProfileRecovery.Preserve(_host, "route", LegacyRosterKey, json, error);
|
||||||
|
_host.Log.Warn(RecoveryNotice);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private T? Read<T>(string key) where T : class
|
private void MigrateLegacyIfNeeded(NavigationSettings target, ISpellCatalog spells)
|
||||||
|
{
|
||||||
|
string fileName = CurrentFileName();
|
||||||
|
if (!VtankStorage.IsAvailable || VtankStorage.ReadText(fileName) is not null)
|
||||||
|
{
|
||||||
|
_pendingLegacyBareName = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string legacyKey = _pendingLegacyBareName is { Length: > 0 } bareName
|
||||||
|
? LegacyProfileKey(bareName, byCharacter: false)
|
||||||
|
: LegacyProfileKey(_characterName, byCharacter: true);
|
||||||
|
LegacyRouteDocument? legacy = ReadLegacyJson(legacyKey);
|
||||||
|
if (legacy is null)
|
||||||
|
{
|
||||||
|
_pendingLegacyBareName = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
legacy.ApplyRouteOnly(target);
|
||||||
|
WriteAf(fileName, MetafSerializer.SaveNav(target));
|
||||||
|
if (_host.Storage.IsAvailable)
|
||||||
|
_host.Storage.Delete(legacyKey);
|
||||||
|
_pendingLegacyBareName = null;
|
||||||
|
_host.Log.Warn($"Migrated legacy MossTank route profile '{legacyKey}' to '{fileName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private LegacyRouteDocument? ReadLegacyJson(string key)
|
||||||
{
|
{
|
||||||
if (!_host.Storage.IsAvailable)
|
if (!_host.Storage.IsAvailable)
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -229,28 +385,56 @@ internal sealed class MossTankRouteProfileStore
|
||||||
json = _host.Storage.ReadText(key);
|
json = _host.Storage.ReadText(key);
|
||||||
return string.IsNullOrWhiteSpace(json)
|
return string.IsNullOrWhiteSpace(json)
|
||||||
? null
|
? null
|
||||||
: JsonSerializer.Deserialize<T>(json, Options);
|
: JsonSerializer.Deserialize<LegacyRouteDocument>(json, JsonOptions);
|
||||||
}
|
}
|
||||||
catch (Exception error)
|
catch (Exception error)
|
||||||
{
|
{
|
||||||
RecoveryNotice = MossTankProfileRecovery.Preserve(
|
RecoveryNotice = MossTankProfileRecovery.Preserve(_host, "route", key, json, error);
|
||||||
_host,
|
|
||||||
"route",
|
|
||||||
key,
|
|
||||||
json,
|
|
||||||
error);
|
|
||||||
_host.Log.Warn(RecoveryNotice);
|
_host.Log.Warn(RecoveryNotice);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Write<T>(string key, T document)
|
// ------------------------------------------------------------------
|
||||||
|
// File naming, storage plumbing.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Round 3 item 3: the hidden "--" prefix MUST come before the "nav_"
|
||||||
|
// kind marker (--nav_Name_Server.af) — putting the marker first
|
||||||
|
// (nav_--Name_Server.af, the pre-fix shape) means the filename does not
|
||||||
|
// start with "--" at all, defeating the StartsWith("--") hidden-file
|
||||||
|
// check both ListNavigationProfiles and ListMetaProfiles rely on and
|
||||||
|
// leaking this character's private per-character route to every other
|
||||||
|
// character's picker.
|
||||||
|
private string CurrentFileName() => _selected.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? VtankProfileDirectory.AutoCharacterFileName(_characterName, Server, "af", NavPrefix)
|
||||||
|
: _selected;
|
||||||
|
|
||||||
|
private static string ToFileName(string bareName) =>
|
||||||
|
NavPrefix + bareName + ".af";
|
||||||
|
|
||||||
|
private void WriteBinding()
|
||||||
{
|
{
|
||||||
if (!_host.Storage.IsAvailable)
|
if (!CanBindFiles || !VtankStorage.IsAvailable)
|
||||||
|
return;
|
||||||
|
VtankProfileDirectory.VtankCharacterBinding existing =
|
||||||
|
VtankProfileDirectory.TryReadCharacterBinding(VtankStorage, _characterName, Server)
|
||||||
|
?? new VtankProfileDirectory.VtankCharacterBinding(
|
||||||
|
string.Empty, string.Empty, CurrentFileName(), null);
|
||||||
|
VtankProfileDirectory.WriteCharacterBinding(
|
||||||
|
VtankStorage,
|
||||||
|
_characterName,
|
||||||
|
Server,
|
||||||
|
existing with { NavFileName = CurrentFileName() });
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteAf(string fileName, string text)
|
||||||
|
{
|
||||||
|
if (!VtankStorage.IsAvailable)
|
||||||
return;
|
return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_host.Storage.WriteText(key, JsonSerializer.Serialize(document, Options));
|
VtankStorage.WriteText(fileName, text);
|
||||||
}
|
}
|
||||||
catch (Exception error)
|
catch (Exception error)
|
||||||
{
|
{
|
||||||
|
|
@ -258,47 +442,35 @@ internal sealed class MossTankRouteProfileStore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveIndex() => Write(IndexKey, _index);
|
private static string Normalize(string? name) => name?.Trim() ?? string.Empty;
|
||||||
|
|
||||||
private void WriteLegacyExport(string name, NavigationSettings settings)
|
private static string LegacyProfileKey(string value, bool byCharacter)
|
||||||
{
|
{
|
||||||
if (!_host.Storage.IsAvailable)
|
string identity = (byCharacter ? "char:" : "named:") + value.Trim().ToUpperInvariant();
|
||||||
return;
|
string hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(identity)));
|
||||||
try
|
return $"profiles/route/{hash}.json";
|
||||||
{
|
|
||||||
_host.Storage.WriteText(
|
|
||||||
$"exports/{LegacyFileName(name)}.nav",
|
|
||||||
VtankNavRouteSerializer.Save(settings));
|
|
||||||
}
|
|
||||||
catch (Exception error)
|
|
||||||
{
|
|
||||||
_host.Log.Warn(
|
|
||||||
$"MossTank VTank navigation export could not be saved: {error.Message}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string LegacyFileName(string name)
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
{
|
{
|
||||||
char[] invalid = Path.GetInvalidFileNameChars();
|
WriteIndented = true,
|
||||||
var result = new StringBuilder(name.Length);
|
PropertyNameCaseInsensitive = true,
|
||||||
foreach (char value in name.Trim())
|
};
|
||||||
{
|
|
||||||
result.Append(value is '/' or '\\' || invalid.Contains(value)
|
|
||||||
? '_'
|
|
||||||
: value);
|
|
||||||
}
|
|
||||||
return result.Length == 0 ? "Route" : result.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class IndexDocument
|
// Read-only: the pre-cutover roster shape at LegacyRosterKey, kept
|
||||||
|
// solely so SweepLegacyRosterIfNeeded can recover it once.
|
||||||
|
private sealed class LegacyRosterDocument
|
||||||
{
|
{
|
||||||
public int Version { get; set; } = 1;
|
|
||||||
public List<string> Names { get; set; } = [];
|
public List<string> Names { get; set; } = [];
|
||||||
public Dictionary<string, string> SelectedByCharacter { get; set; } =
|
|
||||||
new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class RouteDocument
|
// ------------------------------------------------------------------
|
||||||
|
// Migration-only: the OLD full route document shape, kept solely so a
|
||||||
|
// not-yet-migrated JSON profile can still be read once and converted.
|
||||||
|
// Only the fields this store still owns (Mode/Waypoints/FollowTarget)
|
||||||
|
// are applied — the rest belonged to the Settings profile even before
|
||||||
|
// the cutover and are left to whatever that profile already holds.
|
||||||
|
private sealed class LegacyRouteDocument
|
||||||
{
|
{
|
||||||
public int Version { get; set; } = 1;
|
public int Version { get; set; } = 1;
|
||||||
public bool Enabled { get; set; }
|
public bool Enabled { get; set; }
|
||||||
|
|
@ -312,50 +484,20 @@ internal sealed class MossTankRouteProfileStore
|
||||||
public double DoorIdentifyRangeMeters { get; set; } = 20d;
|
public double DoorIdentifyRangeMeters { get; set; } = 20d;
|
||||||
public double DoorOpenRangeMeters { get; set; } = 4d;
|
public double DoorOpenRangeMeters { get; set; } = 4d;
|
||||||
public int DoorLockpickExcessThreshold { get; set; } = -50;
|
public int DoorLockpickExcessThreshold { get; set; } = -50;
|
||||||
public WaypointDocument[] Waypoints { get; set; } = [];
|
public LegacyWaypointDocument[] Waypoints { get; set; } = [];
|
||||||
|
|
||||||
public static RouteDocument Capture(NavigationSettings value) => new()
|
public void ApplyRouteOnly(NavigationSettings value)
|
||||||
{
|
{
|
||||||
Enabled = value.Enabled,
|
|
||||||
Priority = value.Priority,
|
|
||||||
Mode = value.Mode,
|
|
||||||
MinimumDistanceMeters = value.MinimumDistanceMeters,
|
|
||||||
FollowTargetObjectId = value.FollowTargetObjectId,
|
|
||||||
FollowTargetName = value.FollowTargetName,
|
|
||||||
FollowAroundCorners = value.FollowAroundCorners,
|
|
||||||
OpenDoors = value.OpenDoors,
|
|
||||||
DoorIdentifyRangeMeters = value.DoorIdentifyRangeMeters,
|
|
||||||
DoorOpenRangeMeters = value.DoorOpenRangeMeters,
|
|
||||||
DoorLockpickExcessThreshold = value.DoorLockpickExcessThreshold,
|
|
||||||
Waypoints = value.Waypoints.Select(WaypointDocument.From).ToArray(),
|
|
||||||
};
|
|
||||||
|
|
||||||
public void Apply(NavigationSettings value)
|
|
||||||
{
|
|
||||||
value.Enabled = Enabled;
|
|
||||||
value.Priority = Priority;
|
|
||||||
value.Mode = Enum.IsDefined(Mode) ? Mode : RouteMode.Circular;
|
value.Mode = Enum.IsDefined(Mode) ? Mode : RouteMode.Circular;
|
||||||
value.MinimumDistanceMeters = Math.Clamp(
|
|
||||||
MinimumDistanceMeters,
|
|
||||||
0.5d,
|
|
||||||
50d);
|
|
||||||
value.FollowTargetObjectId = FollowTargetObjectId;
|
value.FollowTargetObjectId = FollowTargetObjectId;
|
||||||
value.FollowTargetName = FollowTargetName ?? string.Empty;
|
value.FollowTargetName = FollowTargetName ?? string.Empty;
|
||||||
value.FollowAroundCorners = FollowAroundCorners;
|
|
||||||
value.OpenDoors = OpenDoors;
|
|
||||||
value.DoorIdentifyRangeMeters = Math.Clamp(
|
|
||||||
DoorIdentifyRangeMeters, 1d, 100d);
|
|
||||||
value.DoorOpenRangeMeters = Math.Clamp(
|
|
||||||
DoorOpenRangeMeters, 0.5d, value.DoorIdentifyRangeMeters);
|
|
||||||
value.DoorLockpickExcessThreshold = Math.Clamp(
|
|
||||||
DoorLockpickExcessThreshold, -500, 500);
|
|
||||||
value.Waypoints.Clear();
|
value.Waypoints.Clear();
|
||||||
foreach (WaypointDocument waypoint in Waypoints ?? [])
|
foreach (LegacyWaypointDocument waypoint in Waypoints ?? [])
|
||||||
value.Waypoints.Add(waypoint.ToWaypoint());
|
value.Waypoints.Add(waypoint.ToWaypoint());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class WaypointDocument
|
private sealed class LegacyWaypointDocument
|
||||||
{
|
{
|
||||||
public RouteWaypointType Type { get; set; }
|
public RouteWaypointType Type { get; set; }
|
||||||
public uint CellId { get; set; }
|
public uint CellId { get; set; }
|
||||||
|
|
@ -364,6 +506,12 @@ internal sealed class MossTankRouteProfileStore
|
||||||
public double Elevation { get; set; }
|
public double Elevation { get; set; }
|
||||||
public float HeadingDegrees { get; set; }
|
public float HeadingDegrees { get; set; }
|
||||||
public bool IsOutdoor { get; set; }
|
public bool IsOutdoor { get; set; }
|
||||||
|
public uint ReferenceCellId { get; set; }
|
||||||
|
public double ReferenceEastWest { get; set; }
|
||||||
|
public double ReferenceNorthSouth { get; set; }
|
||||||
|
public double ReferenceElevation { get; set; }
|
||||||
|
public float ReferenceHeadingDegrees { get; set; }
|
||||||
|
public bool ReferenceIsOutdoor { get; set; }
|
||||||
public uint ObjectId { get; set; }
|
public uint ObjectId { get; set; }
|
||||||
public string ObjectName { get; set; } = string.Empty;
|
public string ObjectName { get; set; } = string.Empty;
|
||||||
public int LegacyObjectClass { get; set; }
|
public int LegacyObjectClass { get; set; }
|
||||||
|
|
@ -378,40 +526,18 @@ internal sealed class MossTankRouteProfileStore
|
||||||
public int JumpChargeMilliseconds { get; set; } = 1000;
|
public int JumpChargeMilliseconds { get; set; } = 1000;
|
||||||
public RouteJumpDirection JumpDirection { get; set; }
|
public RouteJumpDirection JumpDirection { get; set; }
|
||||||
|
|
||||||
public static WaypointDocument From(RouteWaypoint value) => new()
|
|
||||||
{
|
|
||||||
Type = value.Type,
|
|
||||||
CellId = value.Position.CellId,
|
|
||||||
EastWest = value.Position.EastWest,
|
|
||||||
NorthSouth = value.Position.NorthSouth,
|
|
||||||
Elevation = value.Position.Elevation,
|
|
||||||
HeadingDegrees = value.Position.HeadingDegrees,
|
|
||||||
IsOutdoor = value.Position.IsOutdoor,
|
|
||||||
ObjectId = value.ObjectId,
|
|
||||||
ObjectName = value.ObjectName,
|
|
||||||
LegacyObjectClass = value.LegacyObjectClass,
|
|
||||||
LegacyReferenceValid = value.LegacyReferenceValid,
|
|
||||||
Text = value.Text,
|
|
||||||
DurationMilliseconds = value.DurationMilliseconds,
|
|
||||||
Recall = value.Recall,
|
|
||||||
RecallSpellId = value.RecallSpellId,
|
|
||||||
RecallSpellName = value.RecallSpellName,
|
|
||||||
JumpHeadingDegrees = value.JumpHeadingDegrees,
|
|
||||||
JumpRun = value.JumpRun,
|
|
||||||
JumpChargeMilliseconds = value.JumpChargeMilliseconds,
|
|
||||||
JumpDirection = value.JumpDirection,
|
|
||||||
};
|
|
||||||
|
|
||||||
public RouteWaypoint ToWaypoint() => new()
|
public RouteWaypoint ToWaypoint() => new()
|
||||||
{
|
{
|
||||||
Type = Enum.IsDefined(Type) ? Type : RouteWaypointType.Point,
|
Type = Enum.IsDefined(Type) ? Type : RouteWaypointType.Point,
|
||||||
Position = new PluginNavigationPosition(
|
Position = new PluginNavigationPosition(
|
||||||
CellId,
|
CellId, EastWest, NorthSouth, Elevation, HeadingDegrees, IsOutdoor),
|
||||||
EastWest,
|
ReferencePosition = new PluginNavigationPosition(
|
||||||
NorthSouth,
|
ReferenceCellId,
|
||||||
Elevation,
|
ReferenceEastWest,
|
||||||
HeadingDegrees,
|
ReferenceNorthSouth,
|
||||||
IsOutdoor),
|
ReferenceElevation,
|
||||||
|
ReferenceHeadingDegrees,
|
||||||
|
ReferenceIsOutdoor),
|
||||||
ObjectId = ObjectId,
|
ObjectId = ObjectId,
|
||||||
ObjectName = ObjectName ?? string.Empty,
|
ObjectName = ObjectName ?? string.Empty,
|
||||||
LegacyObjectClass = LegacyObjectClass,
|
LegacyObjectClass = LegacyObjectClass,
|
||||||
|
|
@ -421,17 +547,15 @@ internal sealed class MossTankRouteProfileStore
|
||||||
Recall = Enum.IsDefined(Recall) ? Recall : RouteRecallKind.Lifestone,
|
Recall = Enum.IsDefined(Recall) ? Recall : RouteRecallKind.Lifestone,
|
||||||
RecallSpellId = RecallSpellId,
|
RecallSpellId = RecallSpellId,
|
||||||
RecallSpellName = RecallSpellName ?? string.Empty,
|
RecallSpellName = RecallSpellName ?? string.Empty,
|
||||||
JumpHeadingDegrees = float.IsFinite(JumpHeadingDegrees)
|
JumpHeadingDegrees = float.IsFinite(JumpHeadingDegrees) ? JumpHeadingDegrees : 0f,
|
||||||
? JumpHeadingDegrees
|
|
||||||
: 0f,
|
|
||||||
JumpRun = JumpRun,
|
JumpRun = JumpRun,
|
||||||
JumpChargeMilliseconds = Math.Clamp(
|
// Round 3 item 5: no load-time clamp here either — the real
|
||||||
JumpChargeMilliseconds,
|
// bi.a 2000 ms ceiling is retail's EXECUTION-time behavior
|
||||||
0,
|
// (NavigationController.TickJump), not a storage-format limit;
|
||||||
10_000),
|
// this legacy migration path preserves whatever the pre-cutover
|
||||||
JumpDirection = Enum.IsDefined(JumpDirection)
|
// JSON authored, exactly like the .af load path now does.
|
||||||
? JumpDirection
|
JumpChargeMilliseconds = JumpChargeMilliseconds,
|
||||||
: RouteJumpDirection.Forward,
|
JumpDirection = Enum.IsDefined(JumpDirection) ? JumpDirection : RouteJumpDirection.Forward,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,17 @@ internal sealed class RouteWaypoint
|
||||||
{
|
{
|
||||||
public RouteWaypointType Type { get; set; }
|
public RouteWaypointType Type { get; set; }
|
||||||
public PluginNavigationPosition Position { get; set; }
|
public PluginNavigationPosition Position { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The second of the two coordinate triples VTank's own Portal2/UseNPC
|
||||||
|
/// waypoint records carry (metaf's "ptl"/"tlk" nodes: "myx myy myz tgtx
|
||||||
|
/// tgty tgtz" — <c>metaf_monolithic.py:356-357,11482,11618</c>; the
|
||||||
|
/// binary <c>.nav</c> record's own trailing xyz). <see cref="Position"/>
|
||||||
|
/// is where the bot stands to use the object ("myxyz"); this is the
|
||||||
|
/// portal/NPC object's own recorded position ("objxyz"), used to
|
||||||
|
/// disambiguate which object at that name to interact with. Unused for
|
||||||
|
/// every other waypoint type.
|
||||||
|
/// </summary>
|
||||||
|
public PluginNavigationPosition ReferencePosition { get; set; }
|
||||||
public uint ObjectId { get; set; }
|
public uint ObjectId { get; set; }
|
||||||
public string ObjectName { get; set; } = string.Empty;
|
public string ObjectName { get; set; } = string.Empty;
|
||||||
/// <summary>Decal ObjectClass retained for exact VTank NAV interchange.</summary>
|
/// <summary>Decal ObjectClass retained for exact VTank NAV interchange.</summary>
|
||||||
|
|
@ -64,6 +75,7 @@ internal sealed class RouteWaypoint
|
||||||
{
|
{
|
||||||
Type = Type,
|
Type = Type,
|
||||||
Position = Position,
|
Position = Position,
|
||||||
|
ReferencePosition = ReferencePosition,
|
||||||
ObjectId = ObjectId,
|
ObjectId = ObjectId,
|
||||||
ObjectName = ObjectName,
|
ObjectName = ObjectName,
|
||||||
LegacyObjectClass = LegacyObjectClass,
|
LegacyObjectClass = LegacyObjectClass,
|
||||||
|
|
@ -185,6 +197,13 @@ internal sealed class NavigationController
|
||||||
private const double RecallExitDistanceMeters = 2.4d;
|
private const double RecallExitDistanceMeters = 2.4d;
|
||||||
private const double JumpLaunchGraceSeconds = 0.25d;
|
private const double JumpLaunchGraceSeconds = 0.25d;
|
||||||
private const double JumpCompletionTimeoutSeconds = 3d;
|
private const double JumpCompletionTimeoutSeconds = 3d;
|
||||||
|
// Round 3 item 5: retail's real jump-charge ceiling
|
||||||
|
// (refs/vtank/decompiled/bi.cs:502-505, bi.a — "if (A_2 > 2000.0) A_2 =
|
||||||
|
// 2000.0" at the moment the jump starts charging) belongs HERE, at
|
||||||
|
// execution, not at .af/legacy-JSON load — a waypoint's authored
|
||||||
|
// JumpChargeMilliseconds round-trips through storage unclamped; only
|
||||||
|
// the actual in-game charge duration is capped.
|
||||||
|
private const int JumpChargeCeilingMilliseconds = 2000;
|
||||||
private const double CheckpointRetrySeconds = 15d;
|
private const double CheckpointRetrySeconds = 15d;
|
||||||
private const double FollowBreadcrumbSpacingMeters = 0.096d;
|
private const double FollowBreadcrumbSpacingMeters = 0.096d;
|
||||||
private const double FollowPathCaptureRangeMeters = 240d;
|
private const double FollowPathCaptureRangeMeters = 240d;
|
||||||
|
|
@ -711,9 +730,15 @@ internal sealed class NavigationController
|
||||||
StringComparison.OrdinalIgnoreCase));
|
StringComparison.OrdinalIgnoreCase));
|
||||||
if (!currentStillExists)
|
if (!currentStillExists)
|
||||||
{
|
{
|
||||||
|
// Search near the object's own recorded position ("tgtxyz"
|
||||||
|
// in metaf's ptl/tlk grammar), not where the bot was
|
||||||
|
// standing when the waypoint was authored ("myxyz" —
|
||||||
|
// waypoint.Position): the object can be meaningfully far
|
||||||
|
// from the approach point (e.g. a portal at the far end of
|
||||||
|
// a room).
|
||||||
if (!_host.Automation.Navigation.TryFindObject(
|
if (!_host.Automation.Navigation.TryFindObject(
|
||||||
waypoint.ObjectName,
|
waypoint.ObjectName,
|
||||||
waypoint.Position,
|
waypoint.ReferencePosition,
|
||||||
ObjectReacquireRadiusMeters,
|
ObjectReacquireRadiusMeters,
|
||||||
out PluginNavigationObject reacquired))
|
out PluginNavigationObject reacquired))
|
||||||
{
|
{
|
||||||
|
|
@ -941,15 +966,16 @@ internal sealed class NavigationController
|
||||||
}
|
}
|
||||||
|
|
||||||
_jumpChargeElapsed += elapsedSeconds;
|
_jumpChargeElapsed += elapsedSeconds;
|
||||||
bool hold = _jumpChargeElapsed * 1000d
|
int effectiveChargeMilliseconds = Math.Clamp(
|
||||||
< Math.Max(0, waypoint.JumpChargeMilliseconds);
|
waypoint.JumpChargeMilliseconds, 0, JumpChargeCeilingMilliseconds);
|
||||||
|
bool hold = _jumpChargeElapsed * 1000d < effectiveChargeMilliseconds;
|
||||||
if (hold)
|
if (hold)
|
||||||
{
|
{
|
||||||
PluginMovementIntent intent = JumpIntent(waypoint, jump: true);
|
PluginMovementIntent intent = JumpIntent(waypoint, jump: true);
|
||||||
_hadMovementIntent = _host.Automation.Navigation
|
_hadMovementIntent = _host.Automation.Navigation
|
||||||
.SetMovementIntent(intent)
|
.SetMovementIntent(intent)
|
||||||
== PluginNavigationCommandStatus.Accepted;
|
== PluginNavigationCommandStatus.Accepted;
|
||||||
_status = $"Charging jump: {waypoint.JumpChargeMilliseconds}ms.";
|
_status = $"Charging jump: {effectiveChargeMilliseconds}ms.";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
PluginMovementIntent release = JumpIntent(waypoint, jump: false);
|
PluginMovementIntent release = JumpIntent(waypoint, jump: false);
|
||||||
|
|
|
||||||
|
|
@ -117,9 +117,9 @@ internal sealed class PetAutomation
|
||||||
if (!settings.SummonPets || activeOwnedPetCount > 0)
|
if (!settings.SummonPets || activeOwnedPetCount > 0)
|
||||||
return PetAutomationChoice.None;
|
return PetAutomationChoice.None;
|
||||||
|
|
||||||
float range = settings.PetRangeMode == PetRangeMode.Custom
|
float range = (float)(settings.PetRangeMode == PetRangeMode.Custom
|
||||||
? settings.PetCustomRange
|
? settings.PetCustomRange
|
||||||
: settings.MaximumRange;
|
: settings.MaximumRange);
|
||||||
int density = Math.Max(1, settings.PetMonsterDensity);
|
int density = Math.Max(1, settings.PetMonsterDensity);
|
||||||
var eligible = new List<(PluginCombatTarget Target, ResolvedMonsterRule Rule)>();
|
var eligible = new List<(PluginCombatTarget Target, ResolvedMonsterRule Rule)>();
|
||||||
foreach (PluginCombatTarget target in targets)
|
foreach (PluginCombatTarget target in targets)
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,11 @@ public sealed class VitalSettings
|
||||||
public double HelperHealth { get; set; } = 0.20;
|
public double HelperHealth { get; set; } = 0.20;
|
||||||
public double HelperStamina { get; set; } = 0.01;
|
public double HelperStamina { get; set; } = 0.01;
|
||||||
public double HelperMana { get; set; } = 0.01;
|
public double HelperMana { get; set; } = 0.01;
|
||||||
public float HelperHealthDistance { get; set; } = 59.6f;
|
// Declared tDouble in VTank's own Settings table (item I, slice-1 fix
|
||||||
public float HelperStaminaDistance { get; set; } = 59.6f;
|
// round) — see CombatSettings.MaximumRange's doc comment for why.
|
||||||
public float HelperManaDistance { get; set; } = 32f;
|
public double HelperHealthDistance { get; set; } = 59.6d;
|
||||||
|
public double HelperStaminaDistance { get; set; } = 59.6d;
|
||||||
|
public double HelperManaDistance { get; set; } = 32d;
|
||||||
|
|
||||||
public bool HelpOthers { get; set; } = true;
|
public bool HelpOthers { get; set; } = true;
|
||||||
public bool UseHealersHeart { get; set; } = true;
|
public bool UseHealersHeart { get; set; } = true;
|
||||||
|
|
@ -46,6 +48,19 @@ public sealed class VitalSettings
|
||||||
public bool ClearLevelBoostFlagOnCast { get; set; } = true;
|
public bool ClearLevelBoostFlagOnCast { get; set; } = true;
|
||||||
public int DropToPeaceModeRetryCount { get; set; } = 34;
|
public int DropToPeaceModeRetryCount { get; set; } = 34;
|
||||||
public string RechargeHandlerSet { get; set; } = "RechargeHandlerSet";
|
public string RechargeHandlerSet { get; set; } = "RechargeHandlerSet";
|
||||||
|
/// <summary>
|
||||||
|
/// The real VTank <c>RechargeHandlerSet</c> nested table (5 columns:
|
||||||
|
/// Vital/HandlerString/MinPercent/MaxPercent/Stance). Defaults to VTank's
|
||||||
|
/// own shipped 26-row table (<see cref="VtankDefaultSettingsDatabase.DefaultRechargeHandlerRows"/>),
|
||||||
|
/// not an independently hand-typed replica; loading a real <c>.usd</c>
|
||||||
|
/// profile via <see cref="VtankSettingsProfileSerializer"/> replaces it
|
||||||
|
/// with that profile's own table. <see cref="VitalRechargePlanner"/>'s
|
||||||
|
/// hand-ported switch-based ordering is retained only as a defensive
|
||||||
|
/// fallback for the case where every row happens to have been cleared
|
||||||
|
/// explicitly and none match a given (vital, stance, percent) band.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<RechargeHandlerRow> RechargeHandlerRows { get; set; } =
|
||||||
|
VtankDefaultSettingsDatabase.DefaultRechargeHandlerRows;
|
||||||
public bool UseKitsInMagicMode { get; set; } = true;
|
public bool UseKitsInMagicMode { get; set; } = true;
|
||||||
public bool GoToPeaceModeToUseKits { get; set; }
|
public bool GoToPeaceModeToUseKits { get; set; }
|
||||||
public int MinimumHealKitSuccessChance { get; set; } = 95;
|
public int MinimumHealKitSuccessChance { get; set; } = 95;
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,43 @@ internal enum VitalRechargeMethod
|
||||||
Food,
|
Food,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One row of VTank's real <c>RechargeHandlerSet</c> nested table
|
||||||
|
/// (<c>refs/vtank/uTank2.Resources.defaultsettings.usd</c>, Settings row
|
||||||
|
/// 137's Value cell — a 5-column TABLE: Vital/HandlerString/MinPercent/
|
||||||
|
/// MaxPercent/Stance, 26 seed rows verified against the live fixture).
|
||||||
|
/// <c>Vital</c> uses VTank's own raw encoding (1=Health, 2=Stamina,
|
||||||
|
/// 3=Mana — distinct from this port's <see cref="VitalKind"/> which reuses
|
||||||
|
/// AC property-id values); <c>Stance</c> is 1=Magic, 2=Melee/Missile
|
||||||
|
/// (matches this planner's <c>magicMode</c> bool exactly, confirmed by
|
||||||
|
/// reproducing every existing hand-ported default list from the parsed
|
||||||
|
/// table). <c>MinPercent</c>/<c>MaxPercent</c> bound the current-vital
|
||||||
|
/// percentage this row applies at.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct RechargeHandlerRow(
|
||||||
|
int Vital,
|
||||||
|
string HandlerString,
|
||||||
|
int MinPercent,
|
||||||
|
int MaxPercent,
|
||||||
|
int Stance)
|
||||||
|
{
|
||||||
|
internal static VitalKind? ToVitalKind(int vital) => vital switch
|
||||||
|
{
|
||||||
|
1 => VitalKind.Health,
|
||||||
|
2 => VitalKind.Stamina,
|
||||||
|
3 => VitalKind.Mana,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
internal static int FromVitalKind(VitalKind vital) => vital switch
|
||||||
|
{
|
||||||
|
VitalKind.Health => 1,
|
||||||
|
VitalKind.Stamina => 2,
|
||||||
|
VitalKind.Mana => 3,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// VTank's default <c>RechargeHandlerSet</c>, including its stance- and
|
/// VTank's default <c>RechargeHandlerSet</c>, including its stance- and
|
||||||
/// current-percentage-dependent order. The host supplies raw inventory and
|
/// current-percentage-dependent order. The host supplies raw inventory and
|
||||||
|
|
@ -60,7 +97,10 @@ internal static class VitalRechargePlanner
|
||||||
vital,
|
vital,
|
||||||
mode == PluginCombatMode.Magic,
|
mode == PluginCombatMode.Magic,
|
||||||
percent,
|
percent,
|
||||||
settings.RechargeHandlerSet);
|
settings.RechargeHandlerSet,
|
||||||
|
settings.RechargeHandlerRows.Count != 0
|
||||||
|
? settings.RechargeHandlerRows
|
||||||
|
: null);
|
||||||
IReadOnlyList<PluginInventoryItem> items =
|
IReadOnlyList<PluginInventoryItem> items =
|
||||||
automation.Items.CaptureOwnedItems();
|
automation.Items.CaptureOwnedItems();
|
||||||
|
|
||||||
|
|
@ -107,7 +147,7 @@ internal static class VitalRechargePlanner
|
||||||
|
|
||||||
IReadOnlyList<PluginFellowMember> members =
|
IReadOnlyList<PluginFellowMember> members =
|
||||||
automation.Fellowship.CaptureMembers();
|
automation.Fellowship.CaptureMembers();
|
||||||
foreach ((VitalKind vital, double threshold, float distance, uint baseSpell)
|
foreach ((VitalKind vital, double threshold, double distance, uint baseSpell)
|
||||||
in new[]
|
in new[]
|
||||||
{
|
{
|
||||||
(VitalKind.Health, settings.HelperHealth,
|
(VitalKind.Health, settings.HelperHealth,
|
||||||
|
|
@ -118,7 +158,7 @@ internal static class VitalRechargePlanner
|
||||||
settings.HelperManaDistance, (uint)SpellId.GiftOfEssence),
|
settings.HelperManaDistance, (uint)SpellId.GiftOfEssence),
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
PluginFellowMember? target = Lowest(members, vital, threshold, distance);
|
PluginFellowMember? target = Lowest(members, vital, threshold, (float)distance);
|
||||||
if (target is not { } fellow)
|
if (target is not { } fellow)
|
||||||
continue;
|
continue;
|
||||||
if (vital == VitalKind.Health
|
if (vital == VitalKind.Health
|
||||||
|
|
@ -205,12 +245,36 @@ internal static class VitalRechargePlanner
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <paramref name="handlerRows"/> is <see cref="VitalSettings.RechargeHandlerRows"/>,
|
||||||
|
/// which defaults to VTank's own shipped 26-row table
|
||||||
|
/// (<see cref="VtankDefaultSettingsDatabase.DefaultRechargeHandlerRows"/>)
|
||||||
|
/// rather than an empty list, so <see cref="TryHandlersFromRows"/> is the
|
||||||
|
/// path every ordinary call takes. The hardcoded <c>defaults</c> switch
|
||||||
|
/// below is retained ONLY as a defensive net for a caller that
|
||||||
|
/// deliberately passes an empty/null row set or an unrecognized
|
||||||
|
/// (vital, stance, percent) band the real table happens not to cover —
|
||||||
|
/// not as the primary source of truth it used to be before the real
|
||||||
|
/// table became parseable.
|
||||||
|
/// </summary>
|
||||||
internal static IReadOnlyList<VitalRechargeMethod> Handlers(
|
internal static IReadOnlyList<VitalRechargeMethod> Handlers(
|
||||||
VitalKind vital,
|
VitalKind vital,
|
||||||
bool magicMode,
|
bool magicMode,
|
||||||
int currentPercent,
|
int currentPercent,
|
||||||
string? handlerSet = null)
|
string? handlerSet = null,
|
||||||
|
IReadOnlyList<RechargeHandlerRow>? handlerRows = null)
|
||||||
{
|
{
|
||||||
|
if (handlerRows is { Count: > 0 }
|
||||||
|
&& TryHandlersFromRows(
|
||||||
|
vital,
|
||||||
|
magicMode,
|
||||||
|
currentPercent,
|
||||||
|
handlerRows,
|
||||||
|
out VitalRechargeMethod[] fromRows))
|
||||||
|
{
|
||||||
|
return fromRows;
|
||||||
|
}
|
||||||
|
|
||||||
IReadOnlyList<VitalRechargeMethod> defaults;
|
IReadOnlyList<VitalRechargeMethod> defaults;
|
||||||
if (magicMode)
|
if (magicMode)
|
||||||
{
|
{
|
||||||
|
|
@ -288,6 +352,81 @@ internal static class VitalRechargePlanner
|
||||||
: defaults;
|
: defaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VTank's real table-driven handler-order lookup: collect every row
|
||||||
|
/// matching (Vital, Stance) whose [MinPercent,MaxPercent] band contains
|
||||||
|
/// the current percentage, IN FILE ORDER (duplicates included — VTank's
|
||||||
|
/// own shipped table has them, e.g. row 17 of the default table repeats
|
||||||
|
/// "Recharge With Food" for the non-magic Health band; a duplicate is
|
||||||
|
/// harmless, since re-trying the same handler a second time just fails
|
||||||
|
/// identically). Confirmed against every hand-ported default list this
|
||||||
|
/// planner shipped before this table was parseable — see
|
||||||
|
/// <c>docs/research/vtank-kb/01-settings-and-profiles.md</c> row 137 and
|
||||||
|
/// the RechargeHandlerSet fixture. Unknown HandlerString tokens are
|
||||||
|
/// skipped (forward-compatible with a future VTank build's new method).
|
||||||
|
/// </summary>
|
||||||
|
private static bool TryHandlersFromRows(
|
||||||
|
VitalKind vital,
|
||||||
|
bool magicMode,
|
||||||
|
int currentPercent,
|
||||||
|
IReadOnlyList<RechargeHandlerRow> rows,
|
||||||
|
out VitalRechargeMethod[] handlers)
|
||||||
|
{
|
||||||
|
int vitalCode = RechargeHandlerRow.FromVitalKind(vital);
|
||||||
|
int stance = magicMode ? 1 : 2;
|
||||||
|
var matched = new List<VitalRechargeMethod>();
|
||||||
|
foreach (RechargeHandlerRow row in rows)
|
||||||
|
{
|
||||||
|
if (row.Vital != vitalCode
|
||||||
|
|| row.Stance != stance
|
||||||
|
|| currentPercent < row.MinPercent
|
||||||
|
|| currentPercent > row.MaxPercent)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (TryParseHandlerToken(row.HandlerString, out VitalRechargeMethod method))
|
||||||
|
matched.Add(method);
|
||||||
|
}
|
||||||
|
handlers = [.. matched];
|
||||||
|
return handlers.Length != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseHandlerToken(string source, out VitalRechargeMethod method)
|
||||||
|
{
|
||||||
|
string normalized = source.Replace(" ", string.Empty, StringComparison.Ordinal)
|
||||||
|
.Replace("-", string.Empty, StringComparison.Ordinal)
|
||||||
|
.ToLowerInvariant();
|
||||||
|
switch (normalized)
|
||||||
|
{
|
||||||
|
case "regularspell":
|
||||||
|
method = VitalRechargeMethod.RegularSpell;
|
||||||
|
return true;
|
||||||
|
case "staminatohealth":
|
||||||
|
method = VitalRechargeMethod.StaminaToHealth;
|
||||||
|
return true;
|
||||||
|
case "manatohealth":
|
||||||
|
method = VitalRechargeMethod.ManaToHealth;
|
||||||
|
return true;
|
||||||
|
case "healthtostamina":
|
||||||
|
method = VitalRechargeMethod.HealthToStamina;
|
||||||
|
return true;
|
||||||
|
case "healthtomana":
|
||||||
|
method = VitalRechargeMethod.HealthToMana;
|
||||||
|
return true;
|
||||||
|
case "kit":
|
||||||
|
case "kitrecharge":
|
||||||
|
method = VitalRechargeMethod.Kit;
|
||||||
|
return true;
|
||||||
|
case "food":
|
||||||
|
case "rechargewithfood":
|
||||||
|
method = VitalRechargeMethod.Food;
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
method = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string HandlerContext(
|
private static string HandlerContext(
|
||||||
VitalKind vital,
|
VitalKind vital,
|
||||||
bool magicMode,
|
bool magicMode,
|
||||||
|
|
|
||||||
2270
src/AcDream.Plugins.MossTank/VtankDefaultSettings.usd
Normal file
2270
src/AcDream.Plugins.MossTank/VtankDefaultSettings.usd
Normal file
File diff suppressed because it is too large
Load diff
67
src/AcDream.Plugins.MossTank/VtankDefaultSettingsDatabase.cs
Normal file
67
src/AcDream.Plugins.MossTank/VtankDefaultSettingsDatabase.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace AcDream.Plugins.MossTank;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The complete <c>.usd</c> shipped by VTank as
|
||||||
|
/// <c>uTank2.Resources.defaultsettings.usd</c> — every table (Settings,
|
||||||
|
/// MyMonsters, GemFoodItems, ExtraBuffSpells, AntiExtraBuffSpells,
|
||||||
|
/// ItemUseSpecifiers, SettingsCategories, SettingsEnumInfo, AssistItems,
|
||||||
|
/// BuffedItems, RechargeHandlerSet, …), byte-for-byte, not a hand-typed
|
||||||
|
/// C# replica. <see cref="VtankSettingsProfileSerializer.CreateNew"/> seeds
|
||||||
|
/// a brand-new profile from this document (every table, every row's real
|
||||||
|
/// <c>Description</c>/<c>SettingType</c>) and
|
||||||
|
/// <see cref="VitalSettings"/>'s default <c>RechargeHandlerRows</c> is
|
||||||
|
/// parsed from its <c>RechargeHandlerSet</c> table — both replace a prior
|
||||||
|
/// port's independently hand-maintained defaults, which could (and did:
|
||||||
|
/// see the 8fa70c3e4 "26 rows, not 24" fix) silently drift from what VTank
|
||||||
|
/// itself ships.
|
||||||
|
/// </summary>
|
||||||
|
internal static class VtankDefaultSettingsDatabase
|
||||||
|
{
|
||||||
|
private const string ResourceSuffix = ".VtankDefaultSettings.usd";
|
||||||
|
private static readonly Lazy<string> RawText = new(LoadText);
|
||||||
|
private static readonly Lazy<RechargeHandlerRow[]> DefaultRows = new(LoadDefaultRows);
|
||||||
|
|
||||||
|
/// <summary>A fresh, independently mutable parse of the embedded document — every call gets its own object graph.</summary>
|
||||||
|
public static VtankDatabase Parse() => VtankDatabase.Parse(RawText.Value);
|
||||||
|
|
||||||
|
/// <summary>VTank's own shipped <c>RechargeHandlerSet</c> rows (26, in file order).</summary>
|
||||||
|
public static IReadOnlyList<RechargeHandlerRow> DefaultRechargeHandlerRows => DefaultRows.Value;
|
||||||
|
|
||||||
|
private static string LoadText()
|
||||||
|
{
|
||||||
|
Assembly assembly = typeof(VtankDefaultSettingsDatabase).Assembly;
|
||||||
|
string resource = assembly.GetManifestResourceNames().Single(
|
||||||
|
static name => name.EndsWith(ResourceSuffix, StringComparison.Ordinal));
|
||||||
|
using Stream stream = assembly.GetManifestResourceStream(resource)
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"The embedded VTank default settings (.usd) document is missing.");
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
return reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RechargeHandlerRow[] LoadDefaultRows()
|
||||||
|
{
|
||||||
|
VtankDatabase database = VtankDatabase.Parse(RawText.Value);
|
||||||
|
VtankTable? settings = database.Find("Settings");
|
||||||
|
if (settings is null)
|
||||||
|
return [];
|
||||||
|
int nameColumn = settings.ColumnIndex("Setting");
|
||||||
|
int valueColumn = settings.ColumnIndex("Value");
|
||||||
|
if (nameColumn < 0 || valueColumn < 0)
|
||||||
|
return [];
|
||||||
|
foreach (VtankRow row in settings.Rows)
|
||||||
|
{
|
||||||
|
if (!row.Cells[nameColumn].AsString().Equals(
|
||||||
|
"RechargeHandlerSet", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
VtankCell cell = row.Cells[valueColumn];
|
||||||
|
if (cell.Tag == "TABLE" && cell.Table is { } table)
|
||||||
|
return VtankSettingsProfileSerializer.ParseRechargeHandlerSet(table);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,20 +23,33 @@ internal static class VtankLootRequirementEvaluator
|
||||||
[6095] = (28, 80),
|
[6095] = (28, 80),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly IReadOnlyDictionary<uint, (uint Key, double Bonus)>
|
/// <summary>
|
||||||
DoubleSpellBonuses = new Dictionary<uint, (uint, double)>
|
/// VTClassic's static double-spell-bonus table (<c>ComputedItemInfo.cs:88-139</c>).
|
||||||
|
/// <c>Change</c> is the real per-row selector KB doc 05 section 2.2 names
|
||||||
|
/// ("additive unless the static table's <c>Change==1</c>, in which case
|
||||||
|
/// multiplicative", <c>ComputedItemInfo.cs:244</c>) — carried explicitly
|
||||||
|
/// here rather than inferred from whether <c>Bonus</c>'s truncated
|
||||||
|
/// integer part happens to equal 1 (a prior port's proxy, which this
|
||||||
|
/// item replaced: it only worked because every multiplicative bonus in
|
||||||
|
/// this table happens to be in [1.0, 2.0) and every additive one happens
|
||||||
|
/// to be under 1.0 — a coincidence of the current 19 rows, not a rule,
|
||||||
|
/// and it would have silently mis-branched on a future row like an
|
||||||
|
/// additive 1.5 or a multiplicative 2.0+).
|
||||||
|
/// </summary>
|
||||||
|
private static readonly IReadOnlyDictionary<uint, (uint Key, double Bonus, bool Change)>
|
||||||
|
DoubleSpellBonuses = new Dictionary<uint, (uint, double, bool)>
|
||||||
{
|
{
|
||||||
[3251] = (152, .01), [3250] = (152, .03),
|
[3251] = (152, .01, false), [3250] = (152, .03, false),
|
||||||
[4670] = (152, .05), [6098] = (152, .07),
|
[4670] = (152, .05, false), [6098] = (152, .07, false),
|
||||||
[2603] = (VtankDoubleBase + 12, .03),
|
[2603] = (VtankDoubleBase + 12, .03, false),
|
||||||
[2591] = (VtankDoubleBase + 12, .05),
|
[2591] = (VtankDoubleBase + 12, .05, false),
|
||||||
[4666] = (VtankDoubleBase + 12, .07),
|
[4666] = (VtankDoubleBase + 12, .07, false),
|
||||||
[6094] = (VtankDoubleBase + 12, .09),
|
[6094] = (VtankDoubleBase + 12, .09, false),
|
||||||
[2600] = (29, .03), [3985] = (29, .04),
|
[2600] = (29, .03, false), [3985] = (29, .04, false),
|
||||||
[2588] = (29, .05), [4663] = (29, .07), [6091] = (29, .09),
|
[2588] = (29, .05, false), [4663] = (29, .07, false), [6091] = (29, .09, false),
|
||||||
[3201] = (144, 1.05), [3199] = (144, 1.10),
|
[3201] = (144, 1.05, true), [3199] = (144, 1.10, true),
|
||||||
[3202] = (144, 1.15), [3200] = (144, 1.20),
|
[3202] = (144, 1.15, true), [3200] = (144, 1.20, true),
|
||||||
[6086] = (144, 1.25), [6087] = (144, 1.30),
|
[6086] = (144, 1.25, true), [6087] = (144, 1.30, true),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly IReadOnlyDictionary<string, int[]> ArmorColorSlots =
|
private static readonly IReadOnlyDictionary<string, int[]> ArmorColorSlots =
|
||||||
|
|
@ -363,54 +376,103 @@ internal static class VtankLootRequirementEvaluator
|
||||||
: string.Empty,
|
: string.Empty,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The single source of truth for both "what is this int-typed key's
|
||||||
|
/// value" (<see cref="IntValue"/>) and "does this item actually carry a
|
||||||
|
/// base value for this key at all" (<see cref="IntKeyExists"/>) — item J
|
||||||
|
/// (slice-1 fix round) consolidated these from two independently
|
||||||
|
/// hand-maintained key lists that had to be kept in exact sync by hand:
|
||||||
|
/// a named field added to one switch and forgotten in the other would
|
||||||
|
/// silently let <c>BuffedInt</c>'s KeyExists gate treat a real,
|
||||||
|
/// always-present field as "raw property bag only", which for a key the
|
||||||
|
/// item's bag never happens to carry would wrongly refuse a spell bonus
|
||||||
|
/// it should apply. Returns <see langword="false"/> only when
|
||||||
|
/// <paramref name="key"/> is not one of the named
|
||||||
|
/// <see cref="PluginInventoryItem"/> fields AND the raw property bag
|
||||||
|
/// does not carry it either.
|
||||||
|
/// </summary>
|
||||||
|
private static bool TryIntValue(
|
||||||
|
uint key,
|
||||||
|
in PluginInventoryItem item,
|
||||||
|
in PluginItemProperties properties,
|
||||||
|
out int value)
|
||||||
|
{
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case 5: value = item.Burden; return true;
|
||||||
|
case 19: value = item.Value; return true;
|
||||||
|
case 105: value = checked((int)item.Workmanship); return true;
|
||||||
|
case 107: value = item.ItemCurrentMana; return true;
|
||||||
|
case 108: value = item.ItemMaximumMana; return true;
|
||||||
|
case 131: value = checked((int)item.MaterialType); return true;
|
||||||
|
case VtankIntBase + 0: value = checked((int)item.WeenieClassId); return true;
|
||||||
|
case VtankIntBase + 2: value = checked((int)item.ContainerObjectId); return true;
|
||||||
|
case VtankIntBase + 4: value = item.ItemsCapacity; return true;
|
||||||
|
case VtankIntBase + 5: value = item.ContainersCapacity; return true;
|
||||||
|
case VtankIntBase + 6: value = item.StackSize; return true;
|
||||||
|
case VtankIntBase + 7: value = item.MaximumStackSize; return true;
|
||||||
|
case VtankIntBase + 8: value = checked((int)item.SpellId); return true;
|
||||||
|
case VtankIntBase + 9: value = item.ContainerSlot; return true;
|
||||||
|
case VtankIntBase + 10: value = checked((int)item.WielderObjectId); return true;
|
||||||
|
case VtankIntBase + 11: value = checked((int)item.EquippedLocation); return true;
|
||||||
|
case VtankIntBase + 14: value = checked((int)item.ValidLocations); return true;
|
||||||
|
case VtankIntBase + 18: value = checked((int)item.Useability); return true;
|
||||||
|
case VtankIntBase + 23: value = checked((int)item.PublicFlags); return true;
|
||||||
|
case VtankIntBase + 31: value = item.CombatUse; return true;
|
||||||
|
case VtankIntBase + 32: value = item.WeaponSkill; return true;
|
||||||
|
case VtankIntBase + 33: value = item.DamageType; return true;
|
||||||
|
case VtankIntBase + 34: value = item.Damage; return true;
|
||||||
|
case VtankIntBase + 38: value = item.AppraisedSpellIds.Count; return true;
|
||||||
|
default:
|
||||||
|
value = 0;
|
||||||
|
return properties.Ints?.TryGetValue(key, out value) == true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static int IntValue(
|
private static int IntValue(
|
||||||
uint key,
|
uint key,
|
||||||
in PluginInventoryItem item,
|
in PluginInventoryItem item,
|
||||||
in PluginItemProperties properties) => key switch
|
in PluginItemProperties properties) =>
|
||||||
|
TryIntValue(key, item, properties, out int value) ? value : 0;
|
||||||
|
|
||||||
|
/// <summary>See <see cref="TryDoubleValue"/> — the double-side equivalent of <see cref="TryIntValue"/>'s consolidation.</summary>
|
||||||
|
private static bool TryDoubleValue(
|
||||||
|
uint key,
|
||||||
|
in PluginInventoryItem item,
|
||||||
|
in PluginItemProperties properties,
|
||||||
|
out double value)
|
||||||
{
|
{
|
||||||
5 => item.Burden,
|
switch (key)
|
||||||
19 => item.Value,
|
{
|
||||||
105 => checked((int)item.Workmanship),
|
case VtankDoubleBase + 9: value = item.Workmanship; return true;
|
||||||
107 => item.ItemCurrentMana,
|
case VtankDoubleBase + 11: value = item.DamageVariance; return true;
|
||||||
108 => item.ItemMaximumMana,
|
// These two are named/virtual fields (like the int side's
|
||||||
131 => checked((int)item.MaterialType),
|
// VtankIntBase+N cases) that always "exist", regardless of
|
||||||
VtankIntBase + 0 => checked((int)item.WeenieClassId),
|
// whether the underlying remapped raw key (62/63) happens to
|
||||||
VtankIntBase + 2 => checked((int)item.ContainerObjectId),
|
// be present in the property bag — matching the prior
|
||||||
VtankIntBase + 4 => item.ItemsCapacity,
|
// DoubleKeyExists switch's explicit "=> true" for both.
|
||||||
VtankIntBase + 5 => item.ContainersCapacity,
|
case VtankDoubleBase + 12:
|
||||||
VtankIntBase + 6 => item.StackSize,
|
TryRawFloat(properties, 62, out value);
|
||||||
VtankIntBase + 7 => item.MaximumStackSize,
|
return true;
|
||||||
VtankIntBase + 8 => checked((int)item.SpellId),
|
case VtankDoubleBase + 14:
|
||||||
VtankIntBase + 9 => item.ContainerSlot,
|
TryRawFloat(properties, 63, out value);
|
||||||
VtankIntBase + 10 => checked((int)item.WielderObjectId),
|
return true;
|
||||||
VtankIntBase + 11 => checked((int)item.EquippedLocation),
|
default:
|
||||||
VtankIntBase + 14 => checked((int)item.ValidLocations),
|
return TryRawFloat(properties, key, out value);
|
||||||
VtankIntBase + 18 => checked((int)item.Useability),
|
}
|
||||||
VtankIntBase + 23 => checked((int)item.PublicFlags),
|
}
|
||||||
VtankIntBase + 31 => item.CombatUse,
|
|
||||||
VtankIntBase + 32 => item.WeaponSkill,
|
|
||||||
VtankIntBase + 33 => item.DamageType,
|
|
||||||
VtankIntBase + 34 => item.Damage,
|
|
||||||
VtankIntBase + 38 => item.AppraisedSpellIds.Count,
|
|
||||||
_ => properties.Ints?.TryGetValue(key, out int value) == true
|
|
||||||
? value
|
|
||||||
: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
private static double DoubleValue(
|
private static double DoubleValue(
|
||||||
uint key,
|
uint key,
|
||||||
in PluginInventoryItem item,
|
in PluginInventoryItem item,
|
||||||
in PluginItemProperties properties) => key switch
|
in PluginItemProperties properties) =>
|
||||||
{
|
TryDoubleValue(key, item, properties, out double value) ? value : 0d;
|
||||||
VtankDoubleBase + 9 => item.Workmanship,
|
|
||||||
VtankDoubleBase + 11 => item.DamageVariance,
|
|
||||||
VtankDoubleBase + 12 => RawFloat(properties, 62),
|
|
||||||
VtankDoubleBase + 14 => RawFloat(properties, 63),
|
|
||||||
_ => RawFloat(properties, key),
|
|
||||||
};
|
|
||||||
|
|
||||||
private static double RawFloat(in PluginItemProperties properties, uint key) =>
|
private static bool TryRawFloat(in PluginItemProperties properties, uint key, out double value)
|
||||||
properties.Floats?.TryGetValue(key, out double value) == true ? value : 0d;
|
{
|
||||||
|
value = 0d;
|
||||||
|
return properties.Floats?.TryGetValue(key, out value) == true;
|
||||||
|
}
|
||||||
|
|
||||||
private static int BuffedInt(
|
private static int BuffedInt(
|
||||||
uint key,
|
uint key,
|
||||||
|
|
@ -418,6 +480,15 @@ internal static class VtankLootRequirementEvaluator
|
||||||
in PluginItemProperties properties)
|
in PluginItemProperties properties)
|
||||||
{
|
{
|
||||||
int value = IntValue(key, item, properties);
|
int value = IntValue(key, item, properties);
|
||||||
|
// VTClassic's ComputedItemInfo.GetBuffedLogValueKey only adds the
|
||||||
|
// spell bonus if the item already carries the base key at all
|
||||||
|
// (ComputedItemInfo.cs:205, the KeyExistsInt gate — KB doc 05
|
||||||
|
// section 2.2 and the section-5 gap-4 finding). Without this gate,
|
||||||
|
// an item that lacks the key entirely (falls through to the
|
||||||
|
// zero-defaulted "_ =>" branch below) would still receive a spell
|
||||||
|
// bonus it never had a base value to buff.
|
||||||
|
if (!IntKeyExists(key, item, properties))
|
||||||
|
return value;
|
||||||
foreach (uint spellId in item.AppraisedSpellIds)
|
foreach (uint spellId in item.AppraisedSpellIds)
|
||||||
{
|
{
|
||||||
if (IntSpellBonuses.TryGetValue(spellId, out var bonus)
|
if (IntSpellBonuses.TryGetValue(spellId, out var bonus)
|
||||||
|
|
@ -435,6 +506,8 @@ internal static class VtankLootRequirementEvaluator
|
||||||
in PluginItemProperties properties)
|
in PluginItemProperties properties)
|
||||||
{
|
{
|
||||||
double value = DoubleValue(key, item, properties);
|
double value = DoubleValue(key, item, properties);
|
||||||
|
if (!DoubleKeyExists(key, item, properties))
|
||||||
|
return value;
|
||||||
foreach (uint spellId in item.AppraisedSpellIds)
|
foreach (uint spellId in item.AppraisedSpellIds)
|
||||||
{
|
{
|
||||||
if (!DoubleSpellBonuses.TryGetValue(spellId, out var bonus)
|
if (!DoubleSpellBonuses.TryGetValue(spellId, out var bonus)
|
||||||
|
|
@ -442,11 +515,28 @@ internal static class VtankLootRequirementEvaluator
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
value = (int)bonus.Bonus == 1 ? value * bonus.Bonus : value + bonus.Bonus;
|
value = bonus.Change ? value * bonus.Bonus : value + bonus.Bonus;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Now driven by <see cref="TryIntValue"/> (item J, slice-1 fix round)
|
||||||
|
/// instead of an independently hand-maintained duplicate of its key
|
||||||
|
/// list — see that method's doc for why the duplication was a hazard.
|
||||||
|
/// </summary>
|
||||||
|
private static bool IntKeyExists(
|
||||||
|
uint key,
|
||||||
|
in PluginInventoryItem item,
|
||||||
|
in PluginItemProperties properties) =>
|
||||||
|
TryIntValue(key, item, properties, out _);
|
||||||
|
|
||||||
|
private static bool DoubleKeyExists(
|
||||||
|
uint key,
|
||||||
|
in PluginInventoryItem item,
|
||||||
|
in PluginItemProperties properties) =>
|
||||||
|
TryDoubleValue(key, item, properties, out _);
|
||||||
|
|
||||||
private static double MinimumDamage(in PluginInventoryItem item) =>
|
private static double MinimumDamage(in PluginInventoryItem item) =>
|
||||||
item.Damage - (item.DamageVariance * item.Damage);
|
item.Damage - (item.DamageVariance * item.Damage);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using AcDream.Plugin.Abstractions;
|
||||||
|
|
||||||
namespace AcDream.Plugins.MossTank;
|
namespace AcDream.Plugins.MossTank;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads and writes VTank's exact line-encoded <c>CondAct</c> Meta database.
|
/// Reads VTank's exact line-encoded <c>CondAct</c> Meta database — a
|
||||||
/// The format is the public interchange contract used by legacy <c>.met</c>
|
/// one-shot import path only. Per the owner's 2026-09-06 "MossTank does not
|
||||||
/// profiles; it is deliberately independent from MossTank's native JSON store.
|
/// implement <c>.met</c>" direction (Campaign VT slice 1 Part A), the
|
||||||
|
/// writer that used to live here was deleted: <c>.af</c>
|
||||||
|
/// (<see cref="MetafSerializer"/>) is the only storage/authoring format for
|
||||||
|
/// meta profiles now. This class only converts a legacy binary <c>.met</c>
|
||||||
|
/// file into a <see cref="MetaProfile"/> so it can be saved straight back
|
||||||
|
/// out as <c>.af</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class VtankMetaProfileSerializer
|
internal static class VtankMetaProfileSerializer
|
||||||
{
|
{
|
||||||
|
|
@ -20,8 +26,16 @@ internal static class VtankMetaProfileSerializer
|
||||||
private const int MaximumRules = 100_000;
|
private const int MaximumRules = 100_000;
|
||||||
private const int MaximumNesting = 256;
|
private const int MaximumNesting = 256;
|
||||||
|
|
||||||
public static bool TryLoad(string source, out MetaProfile profile, out string error)
|
public static bool TryLoad(string source, out MetaProfile profile, out string error) =>
|
||||||
|
TryLoad(source, MetafSerializer.NoOpSpells.Instance, out profile, out error);
|
||||||
|
|
||||||
|
public static bool TryLoad(
|
||||||
|
string source,
|
||||||
|
ISpellCatalog spells,
|
||||||
|
out MetaProfile profile,
|
||||||
|
out string error)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(spells);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var reader = new LineReader(source);
|
var reader = new LineReader(source);
|
||||||
|
|
@ -38,7 +52,7 @@ internal static class VtankMetaProfileSerializer
|
||||||
reader.Expect("i");
|
reader.Expect("i");
|
||||||
int actionType = reader.ReadInt();
|
int actionType = reader.ReadInt();
|
||||||
MetaCondition condition = ReadCondition(reader, conditionType, 0);
|
MetaCondition condition = ReadCondition(reader, conditionType, 0);
|
||||||
MetaAction action = ReadAction(reader, actionType, 0);
|
MetaAction action = ReadAction(reader, actionType, 0, spells);
|
||||||
reader.Expect("s");
|
reader.Expect("s");
|
||||||
parsed.Rules.Add(new MetaRule
|
parsed.Rules.Add(new MetaRule
|
||||||
{
|
{
|
||||||
|
|
@ -62,23 +76,6 @@ internal static class VtankMetaProfileSerializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string Save(MetaProfile source)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(source);
|
|
||||||
var writer = new LineWriter();
|
|
||||||
writer.Add(Header);
|
|
||||||
MetaRule[] rules = source.Rules.Where(static rule => rule.Enabled).ToArray();
|
|
||||||
writer.Add(rules.Length);
|
|
||||||
foreach (MetaRule rule in rules)
|
|
||||||
{
|
|
||||||
writer.Add("i", ConditionType(rule.Condition.Kind), "i", ActionType(rule.Action.Kind));
|
|
||||||
WriteCondition(writer, rule.Condition, 0);
|
|
||||||
WriteAction(writer, rule.Action, 0);
|
|
||||||
writer.Add("s", rule.State ?? string.Empty);
|
|
||||||
}
|
|
||||||
return writer.Finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MetaCondition ReadCondition(LineReader reader, int type, int depth)
|
private static MetaCondition ReadCondition(LineReader reader, int type, int depth)
|
||||||
{
|
{
|
||||||
CheckDepth(reader, depth);
|
CheckDepth(reader, depth);
|
||||||
|
|
@ -172,7 +169,7 @@ internal static class VtankMetaProfileSerializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MetaAction ReadAction(LineReader reader, int type, int depth)
|
private static MetaAction ReadAction(LineReader reader, int type, int depth, ISpellCatalog spells)
|
||||||
{
|
{
|
||||||
CheckDepth(reader, depth);
|
CheckDepth(reader, depth);
|
||||||
var value = new MetaAction { Kind = ActionKind(type) };
|
var value = new MetaAction { Kind = ActionKind(type) };
|
||||||
|
|
@ -191,11 +188,11 @@ internal static class VtankMetaProfileSerializer
|
||||||
for (int index = 0; index < count; index++)
|
for (int index = 0; index < count; index++)
|
||||||
{
|
{
|
||||||
reader.Expect("i");
|
reader.Expect("i");
|
||||||
value.Children.Add(ReadAction(reader, reader.ReadInt(), depth + 1));
|
value.Children.Add(ReadAction(reader, reader.ReadInt(), depth + 1, spells));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
ReadEmbeddedNavigation(reader, value);
|
ReadEmbeddedNavigation(reader, value, spells);
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
reader.Expect(TablePrefix, "2", "s", "st", "s");
|
reader.Expect(TablePrefix, "2", "s", "st", "s");
|
||||||
|
|
@ -247,7 +244,7 @@ internal static class VtankMetaProfileSerializer
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ReadEmbeddedNavigation(LineReader reader, MetaAction target)
|
private static void ReadEmbeddedNavigation(LineReader reader, MetaAction target, ISpellCatalog spells)
|
||||||
{
|
{
|
||||||
reader.Expect("ba");
|
reader.Expect("ba");
|
||||||
int serializedCharacters = reader.ReadCount();
|
int serializedCharacters = reader.ReadCount();
|
||||||
|
|
@ -255,7 +252,7 @@ internal static class VtankMetaProfileSerializer
|
||||||
int statedNodeCount = reader.ReadCount();
|
int statedNodeCount = reader.ReadCount();
|
||||||
if (serializedCharacters <= 5)
|
if (serializedCharacters <= 5)
|
||||||
{
|
{
|
||||||
target.Text = EmptyNavigation();
|
target.EmbeddedRoute = new NavigationSettings();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -282,7 +279,15 @@ internal static class VtankMetaProfileSerializer
|
||||||
}
|
}
|
||||||
if (actualNodeCount != statedNodeCount)
|
if (actualNodeCount != statedNodeCount)
|
||||||
throw reader.Error("Embedded VTank navigation node counts do not match.");
|
throw reader.Error("Embedded VTank navigation node counts do not match.");
|
||||||
target.Text = string.Join("\r\n", lines) + "\r\n";
|
|
||||||
|
// Parse the reassembled "uTank2 NAV 1.2" text through the same
|
||||||
|
// reader a top-level binary .nav import uses (item B: MetaAction
|
||||||
|
// carries a typed NavigationSettings, not a re-parse-me blob).
|
||||||
|
string blob = string.Join("\r\n", lines) + "\r\n";
|
||||||
|
var route = new NavigationSettings();
|
||||||
|
if (!VtankNavRouteSerializer.TryLoad(blob, route, spells, out string navError))
|
||||||
|
throw reader.Error($"Embedded VTank navigation route is invalid: {navError}");
|
||||||
|
target.EmbeddedRoute = route;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ReadNavigationNode(LineReader reader, List<string> lines)
|
private static void ReadNavigationNode(LineReader reader, List<string> lines)
|
||||||
|
|
@ -304,195 +309,10 @@ internal static class VtankMetaProfileSerializer
|
||||||
lines.Add(reader.Read());
|
lines.Add(reader.Read());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void WriteCondition(LineWriter writer, MetaCondition value, int depth)
|
|
||||||
{
|
|
||||||
CheckDepth(depth);
|
|
||||||
int type = ConditionType(value.Kind);
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case 0 or 1 or 7 or 8 or 9 or 10 or 15 or 19 or 20:
|
|
||||||
writer.Add("i", "0");
|
|
||||||
break;
|
|
||||||
case 2 or 3:
|
|
||||||
writer.Add(RecursiveTablePrefix);
|
|
||||||
writer.Add(value.Children.Count);
|
|
||||||
foreach (MetaCondition child in value.Children)
|
|
||||||
{
|
|
||||||
writer.Add("i", ConditionType(child.Kind));
|
|
||||||
WriteCondition(writer, child, depth + 1);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
writer.Add("s", value.Text);
|
|
||||||
break;
|
|
||||||
case 5 or 6 or 17 or 18 or 22 or 24:
|
|
||||||
writer.Add("i", IntValue(value.Number));
|
|
||||||
break;
|
|
||||||
case 11 or 12:
|
|
||||||
writer.Add(TablePrefix, "2", "s", "n", "s", value.Text,
|
|
||||||
"s", "c", "i", IntValue(value.Number));
|
|
||||||
break;
|
|
||||||
case 13:
|
|
||||||
writer.Add(TablePrefix, "3", "s", "n", "s", value.Text,
|
|
||||||
"s", "c", "i", IntValue(value.Number),
|
|
||||||
"s", "r", "d", Number(value.SecondaryNumber));
|
|
||||||
break;
|
|
||||||
case 14:
|
|
||||||
writer.Add(TablePrefix, "3", "s", "p", "i", IntValue(value.TertiaryNumber),
|
|
||||||
"s", "c", "i", IntValue(value.Number),
|
|
||||||
"s", "r", "d", Number(value.SecondaryNumber));
|
|
||||||
break;
|
|
||||||
case 16:
|
|
||||||
writer.Add(TablePrefix, "1", "s", "r", "d", Number(value.Number));
|
|
||||||
break;
|
|
||||||
case 21:
|
|
||||||
if (value.Children.Count != 1)
|
|
||||||
throw new InvalidOperationException("VTank Meta Not requires exactly one condition.");
|
|
||||||
writer.Add(RecursiveTablePrefix, "1", "i", ConditionType(value.Children[0].Kind));
|
|
||||||
WriteCondition(writer, value.Children[0], depth + 1);
|
|
||||||
break;
|
|
||||||
case 23:
|
|
||||||
writer.Add(TablePrefix, "2", "s", "sid", "i", IntValue(value.Number),
|
|
||||||
"s", "sec", "i", IntValue(value.SecondaryNumber));
|
|
||||||
break;
|
|
||||||
case 25:
|
|
||||||
writer.Add(TablePrefix, "1", "s", "dist", "d", Number(value.Number));
|
|
||||||
break;
|
|
||||||
case 26:
|
|
||||||
writer.Add(TablePrefix, "1", "s", "e", "s", value.Text);
|
|
||||||
break;
|
|
||||||
case 28:
|
|
||||||
writer.Add(TablePrefix, "2", "s", "p", "s", value.Text,
|
|
||||||
"s", "c", "s", value.SecondaryText);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new InvalidOperationException($"Unknown VTank Meta condition type {type}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteAction(LineWriter writer, MetaAction value, int depth)
|
|
||||||
{
|
|
||||||
CheckDepth(depth);
|
|
||||||
int type = ActionType(value.Kind);
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case 0 or 6:
|
|
||||||
writer.Add("i", "0");
|
|
||||||
break;
|
|
||||||
case 1 or 2:
|
|
||||||
writer.Add("s", value.Text);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
writer.Add(RecursiveTablePrefix);
|
|
||||||
writer.Add(value.Children.Count);
|
|
||||||
foreach (MetaAction child in value.Children)
|
|
||||||
{
|
|
||||||
writer.Add("i", ActionType(child.Kind));
|
|
||||||
WriteAction(writer, child, depth + 1);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
WriteEmbeddedNavigation(writer, value);
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
writer.Add(TablePrefix, "2", "s", "st", "s", value.Text,
|
|
||||||
"s", "ret", "s", value.SecondaryText);
|
|
||||||
break;
|
|
||||||
case 7 or 8:
|
|
||||||
writer.Add(TablePrefix, "1", "s", "e", "s", value.Text);
|
|
||||||
break;
|
|
||||||
case 9:
|
|
||||||
writer.Add(TablePrefix, "3", "s", "s", "s", value.Text,
|
|
||||||
"s", "r", "d", Number(value.Number),
|
|
||||||
"s", "t", "d", Number(value.SecondaryNumber));
|
|
||||||
break;
|
|
||||||
case 10 or 15:
|
|
||||||
writer.Add(TablePrefix, "0");
|
|
||||||
break;
|
|
||||||
case 11 or 12:
|
|
||||||
writer.Add(TablePrefix, "2", "s", "o", "s", value.Text,
|
|
||||||
"s", "v", "s", value.SecondaryText);
|
|
||||||
break;
|
|
||||||
case 13:
|
|
||||||
writer.Add(TablePrefix, "2", "s", "n", "s", value.Text,
|
|
||||||
"s", "x", "ba", value.SecondaryText.Length);
|
|
||||||
writer.AddBuggedByteArray(value.SecondaryText);
|
|
||||||
break;
|
|
||||||
case 14:
|
|
||||||
writer.Add(TablePrefix, "1", "s", "n", "s", value.Text);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new InvalidOperationException($"Unknown VTank Meta action type {type}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteEmbeddedNavigation(LineWriter writer, MetaAction value)
|
|
||||||
{
|
|
||||||
string nav = string.IsNullOrWhiteSpace(value.Text) ? EmptyNavigation() : value.Text;
|
|
||||||
string normalized = NormalizeNewlines(nav);
|
|
||||||
string[] navLines = normalized.Split('\n', StringSplitOptions.None);
|
|
||||||
if (navLines.Length != 0 && navLines[^1].Length == 0)
|
|
||||||
navLines = navLines[..^1];
|
|
||||||
int nodes = NavigationNodeCount(navLines);
|
|
||||||
string name = string.IsNullOrEmpty(value.SecondaryText) ? "[None]" : value.SecondaryText;
|
|
||||||
int characters = name.Length + 2
|
|
||||||
+ nodes.ToString(CultureInfo.InvariantCulture).Length + 2
|
|
||||||
+ navLines.Sum(static line => line.Length + 2);
|
|
||||||
writer.Add("ba", characters, name, nodes);
|
|
||||||
writer.Add(navLines);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int NavigationNodeCount(string[] lines)
|
|
||||||
{
|
|
||||||
if (lines.Length < 2 || !lines[0].Equals("uTank2 NAV 1.2", StringComparison.Ordinal))
|
|
||||||
throw new InvalidOperationException("Embedded Meta route is not uTank2 NAV 1.2 data.");
|
|
||||||
int mode = int.Parse(lines[1], NumberStyles.Integer, CultureInfo.InvariantCulture);
|
|
||||||
if (mode == 3)
|
|
||||||
return 1;
|
|
||||||
if (mode is not (1 or 2 or 4) || lines.Length < 3)
|
|
||||||
throw new InvalidOperationException("Embedded Meta route has an invalid navigation type.");
|
|
||||||
return int.Parse(lines[2], NumberStyles.Integer, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string EmptyNavigation() => "uTank2 NAV 1.2\r\n1\r\n0\r\n";
|
|
||||||
|
|
||||||
private static string NormalizeNewlines(string value) => value
|
private static string NormalizeNewlines(string value) => value
|
||||||
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||||
.Replace('\r', '\n');
|
.Replace('\r', '\n');
|
||||||
|
|
||||||
private static int ConditionType(MetaConditionKind kind) => kind switch
|
|
||||||
{
|
|
||||||
MetaConditionKind.Never => 0,
|
|
||||||
MetaConditionKind.Always => 1,
|
|
||||||
MetaConditionKind.All => 2,
|
|
||||||
MetaConditionKind.Any => 3,
|
|
||||||
MetaConditionKind.ChatMessage => 4,
|
|
||||||
MetaConditionKind.PackSlotsLessThanOrEqual => 5,
|
|
||||||
MetaConditionKind.SecondsInStateGreaterThanOrEqual => 6,
|
|
||||||
MetaConditionKind.NavigationRouteEmpty => 7,
|
|
||||||
MetaConditionKind.CharacterDeath => 8,
|
|
||||||
MetaConditionKind.AnyVendorOpen => 9,
|
|
||||||
MetaConditionKind.VendorClosed => 10,
|
|
||||||
MetaConditionKind.InventoryItemCountLessThanOrEqual => 11,
|
|
||||||
MetaConditionKind.InventoryItemCountGreaterThanOrEqual => 12,
|
|
||||||
MetaConditionKind.MonsterNameCountWithinDistance => 13,
|
|
||||||
MetaConditionKind.MonsterPriorityCountWithinDistance => 14,
|
|
||||||
MetaConditionKind.NeedToBuff => 15,
|
|
||||||
MetaConditionKind.NoMonstersWithinDistance => 16,
|
|
||||||
MetaConditionKind.LandblockEquals => 17,
|
|
||||||
MetaConditionKind.LandcellEquals => 18,
|
|
||||||
MetaConditionKind.PortalspaceEntered => 19,
|
|
||||||
MetaConditionKind.PortalspaceExited => 20,
|
|
||||||
MetaConditionKind.Not => 21,
|
|
||||||
MetaConditionKind.PersistentSecondsInStateGreaterThanOrEqual => 22,
|
|
||||||
MetaConditionKind.TimeLeftOnSpellGreaterThanOrEqual => 23,
|
|
||||||
MetaConditionKind.BurdenPercentGreaterThanOrEqual => 24,
|
|
||||||
MetaConditionKind.DistanceFromAnyRoutePointGreaterThanOrEqual => 25,
|
|
||||||
MetaConditionKind.Expression => 26,
|
|
||||||
MetaConditionKind.ChatMessageCapture => 28,
|
|
||||||
_ => throw new InvalidOperationException($"Unsupported Meta condition {kind}."),
|
|
||||||
};
|
|
||||||
|
|
||||||
private static MetaConditionKind ConditionKind(int type) => type switch
|
private static MetaConditionKind ConditionKind(int type) => type switch
|
||||||
{
|
{
|
||||||
0 => MetaConditionKind.Never,
|
0 => MetaConditionKind.Never,
|
||||||
|
|
@ -526,27 +346,6 @@ internal static class VtankMetaProfileSerializer
|
||||||
_ => throw new FormatException($"Unknown VTank Meta condition type {type}."),
|
_ => throw new FormatException($"Unknown VTank Meta condition type {type}."),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static int ActionType(MetaActionKind kind) => kind switch
|
|
||||||
{
|
|
||||||
MetaActionKind.None => 0,
|
|
||||||
MetaActionKind.SetMetaState => 1,
|
|
||||||
MetaActionKind.ChatCommand => 2,
|
|
||||||
MetaActionKind.All => 3,
|
|
||||||
MetaActionKind.LoadEmbeddedNavigationRoute => 4,
|
|
||||||
MetaActionKind.CallMetaState => 5,
|
|
||||||
MetaActionKind.ReturnFromCall => 6,
|
|
||||||
MetaActionKind.ExpressionAction => 7,
|
|
||||||
MetaActionKind.ChatExpression => 8,
|
|
||||||
MetaActionKind.SetWatchdog => 9,
|
|
||||||
MetaActionKind.ClearWatchdog => 10,
|
|
||||||
MetaActionKind.GetVtankOption => 11,
|
|
||||||
MetaActionKind.SetVtankOption => 12,
|
|
||||||
MetaActionKind.CreateView => 13,
|
|
||||||
MetaActionKind.DestroyView => 14,
|
|
||||||
MetaActionKind.DestroyAllViews => 15,
|
|
||||||
_ => throw new InvalidOperationException($"Unsupported Meta action {kind}."),
|
|
||||||
};
|
|
||||||
|
|
||||||
private static MetaActionKind ActionKind(int type) => type switch
|
private static MetaActionKind ActionKind(int type) => type switch
|
||||||
{
|
{
|
||||||
0 => MetaActionKind.None,
|
0 => MetaActionKind.None,
|
||||||
|
|
@ -568,32 +367,12 @@ internal static class VtankMetaProfileSerializer
|
||||||
_ => throw new FormatException($"Unknown VTank Meta action type {type}."),
|
_ => throw new FormatException($"Unknown VTank Meta action type {type}."),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static string Number(double value)
|
|
||||||
{
|
|
||||||
if (!double.IsFinite(value))
|
|
||||||
throw new InvalidOperationException("VTank Meta numbers must be finite.");
|
|
||||||
return value.ToString("R", CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int IntValue(double value)
|
|
||||||
{
|
|
||||||
if (!double.IsFinite(value) || value != Math.Truncate(value))
|
|
||||||
throw new InvalidOperationException("VTank Meta integer fields require whole numbers.");
|
|
||||||
return checked((int)value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void CheckDepth(LineReader reader, int depth)
|
private static void CheckDepth(LineReader reader, int depth)
|
||||||
{
|
{
|
||||||
if (depth > MaximumNesting)
|
if (depth > MaximumNesting)
|
||||||
throw reader.Error("VTank Meta nesting is too deep.");
|
throw reader.Error("VTank Meta nesting is too deep.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CheckDepth(int depth)
|
|
||||||
{
|
|
||||||
if (depth > MaximumNesting)
|
|
||||||
throw new InvalidOperationException("VTank Meta nesting is too deep.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class LineReader
|
private sealed class LineReader
|
||||||
{
|
{
|
||||||
private readonly List<string> _lines;
|
private readonly List<string> _lines;
|
||||||
|
|
@ -703,40 +482,4 @@ internal static class VtankMetaProfileSerializer
|
||||||
public FormatException Error(string message) =>
|
public FormatException Error(string message) =>
|
||||||
new($"VTank Meta line {Math.Min(_index + 1, _lines.Count + 1)}: {message}");
|
new($"VTank Meta line {Math.Min(_index + 1, _lines.Count + 1)}: {message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class LineWriter
|
|
||||||
{
|
|
||||||
private readonly List<string> _lines = [];
|
|
||||||
private readonly List<int> _buggedByteArrays = [];
|
|
||||||
|
|
||||||
public void Add(params object?[] values)
|
|
||||||
{
|
|
||||||
foreach (object? value in values)
|
|
||||||
_lines.Add(Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(string[] first, params object?[] rest)
|
|
||||||
{
|
|
||||||
Add(first.Cast<object?>().ToArray());
|
|
||||||
Add(rest);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddBuggedByteArray(string value)
|
|
||||||
{
|
|
||||||
_buggedByteArrays.Add(_lines.Count);
|
|
||||||
_lines.Add(value ?? string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Finish()
|
|
||||||
{
|
|
||||||
foreach (int index in _buggedByteArrays.OrderDescending())
|
|
||||||
{
|
|
||||||
if (index + 1 >= _lines.Count)
|
|
||||||
throw new InvalidOperationException("CreateView cannot terminate a VTank Meta record.");
|
|
||||||
_lines[index] += _lines[index + 1];
|
|
||||||
_lines.RemoveAt(index + 1);
|
|
||||||
}
|
|
||||||
return string.Join("\r\n", _lines) + "\r\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,40 +3,27 @@ using AcDream.Plugin.Abstractions;
|
||||||
|
|
||||||
namespace AcDream.Plugins.MossTank;
|
namespace AcDream.Plugins.MossTank;
|
||||||
|
|
||||||
/// <summary>Reader for VTank's verbatim <c>uTank2 NAV 1.2</c> format.</summary>
|
/// <summary>
|
||||||
|
/// Reader for VTank's verbatim <c>uTank2 NAV 1.2</c> format — a one-shot
|
||||||
|
/// import path only. Per the owner's 2026-09-06 "MossTank does not author
|
||||||
|
/// <c>.nav</c>" direction (Campaign VT slice 1 Part A), the writer that
|
||||||
|
/// used to live here was deleted: <c>.af</c> (<see cref="MetafSerializer"/>)
|
||||||
|
/// is the only storage/authoring format for navigation routes now.
|
||||||
|
/// <see cref="MetaEngine"/>'s embedded-navigation contract (a Meta rule's
|
||||||
|
/// <see cref="MetaActionKind.LoadEmbeddedNavigationRoute"/> action,
|
||||||
|
/// <c>.af</c> tag <c>EmbedNav</c>) is a typed
|
||||||
|
/// <see cref="MetaAction.EmbeddedRoute"/> <see cref="NavigationSettings"/>
|
||||||
|
/// now (round 2 step B, replacing an earlier binary-blob shape), saved and
|
||||||
|
/// loaded through the SAME <see cref="MetafSerializer.SaveNav"/>/
|
||||||
|
/// <see cref="MetafSerializer.TryLoadNav"/> grammar this class no longer
|
||||||
|
/// owns any writer for — round 3 item 12 cleanup: this doc comment
|
||||||
|
/// previously cited a "<c>WriteBinaryNavBlob</c>" method that does not
|
||||||
|
/// exist anywhere in the codebase.
|
||||||
|
/// </summary>
|
||||||
internal static class VtankNavRouteSerializer
|
internal static class VtankNavRouteSerializer
|
||||||
{
|
{
|
||||||
private const string Header = "uTank2 NAV 1.2";
|
private const string Header = "uTank2 NAV 1.2";
|
||||||
|
|
||||||
public static string Save(NavigationSettings source)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(source);
|
|
||||||
var writer = new StringWriter(CultureInfo.InvariantCulture)
|
|
||||||
{
|
|
||||||
NewLine = "\r\n",
|
|
||||||
};
|
|
||||||
writer.WriteLine(Header);
|
|
||||||
writer.WriteLine(source.Mode switch
|
|
||||||
{
|
|
||||||
RouteMode.Circular => 1,
|
|
||||||
RouteMode.Linear => 2,
|
|
||||||
RouteMode.Target => 3,
|
|
||||||
RouteMode.Once => 4,
|
|
||||||
_ => throw new InvalidOperationException("Unknown navigation type."),
|
|
||||||
});
|
|
||||||
if (source.Mode == RouteMode.Target)
|
|
||||||
{
|
|
||||||
writer.WriteLine(source.FollowTargetName ?? string.Empty);
|
|
||||||
writer.WriteLine(unchecked((int)source.FollowTargetObjectId));
|
|
||||||
return writer.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteLine(source.Waypoints.Count);
|
|
||||||
foreach (RouteWaypoint waypoint in source.Waypoints)
|
|
||||||
WriteWaypoint(writer, waypoint);
|
|
||||||
return writer.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TryLoad(
|
public static bool TryLoad(
|
||||||
string source,
|
string source,
|
||||||
NavigationSettings target,
|
NavigationSettings target,
|
||||||
|
|
@ -145,13 +132,23 @@ internal static class VtankNavRouteSerializer
|
||||||
break;
|
break;
|
||||||
case 6:
|
case 6:
|
||||||
case 7:
|
case 7:
|
||||||
|
// The record's leading eastWest/northSouth/elevation (read
|
||||||
|
// above into waypoint.Position) is "myxyz" — where the
|
||||||
|
// character stood when the waypoint was authored. This
|
||||||
|
// trailing triple is the portal/NPC object's own recorded
|
||||||
|
// position ("tgtxyz" in metaf's parallel ptl/tlk grammar —
|
||||||
|
// see MetafSerializer.ReadNavNode), kept separately so a
|
||||||
|
// reload can still disambiguate which object at that name
|
||||||
|
// to interact with, matching the record shape exactly:
|
||||||
|
// the pre-existing single-Position port used to overwrite
|
||||||
|
// "myxyz" with this value instead of keeping both.
|
||||||
waypoint.ObjectName = ReadLine(reader);
|
waypoint.ObjectName = ReadLine(reader);
|
||||||
waypoint.LegacyObjectClass = ReadInt(reader);
|
waypoint.LegacyObjectClass = ReadInt(reader);
|
||||||
waypoint.LegacyReferenceValid = ReadBoolean(reader);
|
waypoint.LegacyReferenceValid = ReadBoolean(reader);
|
||||||
double referenceEastWest = ReadDouble(reader);
|
double referenceEastWest = ReadDouble(reader);
|
||||||
double referenceNorthSouth = ReadDouble(reader);
|
double referenceNorthSouth = ReadDouble(reader);
|
||||||
double referenceElevation = ReadDouble(reader);
|
double referenceElevation = ReadDouble(reader);
|
||||||
waypoint.Position = Position(
|
waypoint.ReferencePosition = Position(
|
||||||
referenceEastWest,
|
referenceEastWest,
|
||||||
referenceNorthSouth,
|
referenceNorthSouth,
|
||||||
referenceElevation);
|
referenceElevation);
|
||||||
|
|
@ -165,76 +162,6 @@ internal static class VtankNavRouteSerializer
|
||||||
return waypoint;
|
return waypoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void WriteWaypoint(TextWriter writer, RouteWaypoint waypoint)
|
|
||||||
{
|
|
||||||
int type = (int)waypoint.Type;
|
|
||||||
writer.WriteLine(type.ToString(CultureInfo.InvariantCulture));
|
|
||||||
WriteDouble(writer, waypoint.Position.EastWest);
|
|
||||||
WriteDouble(writer, waypoint.Position.NorthSouth);
|
|
||||||
WriteDouble(writer, waypoint.Position.Elevation);
|
|
||||||
writer.WriteLine("0");
|
|
||||||
switch (waypoint.Type)
|
|
||||||
{
|
|
||||||
case RouteWaypointType.Point:
|
|
||||||
case RouteWaypointType.Checkpoint:
|
|
||||||
break;
|
|
||||||
case RouteWaypointType.Portal:
|
|
||||||
writer.WriteLine(unchecked((int)waypoint.ObjectId)
|
|
||||||
.ToString(CultureInfo.InvariantCulture));
|
|
||||||
break;
|
|
||||||
case RouteWaypointType.Recall:
|
|
||||||
writer.WriteLine(waypoint.RecallSpellId
|
|
||||||
.ToString(CultureInfo.InvariantCulture));
|
|
||||||
break;
|
|
||||||
case RouteWaypointType.Pause:
|
|
||||||
writer.WriteLine(waypoint.DurationMilliseconds
|
|
||||||
.ToString(CultureInfo.InvariantCulture));
|
|
||||||
break;
|
|
||||||
case RouteWaypointType.ChatCommand:
|
|
||||||
writer.WriteLine(waypoint.Text ?? string.Empty);
|
|
||||||
break;
|
|
||||||
case RouteWaypointType.OpenVendor:
|
|
||||||
writer.WriteLine(unchecked((int)waypoint.ObjectId)
|
|
||||||
.ToString(CultureInfo.InvariantCulture));
|
|
||||||
writer.WriteLine(waypoint.ObjectName ?? string.Empty);
|
|
||||||
break;
|
|
||||||
case RouteWaypointType.PortalByName:
|
|
||||||
case RouteWaypointType.UseNpc:
|
|
||||||
writer.WriteLine(waypoint.ObjectName ?? string.Empty);
|
|
||||||
int objectClass = waypoint.LegacyObjectClass != 0
|
|
||||||
? waypoint.LegacyObjectClass
|
|
||||||
: waypoint.Type == RouteWaypointType.PortalByName ? 14 : 37;
|
|
||||||
writer.WriteLine(objectClass.ToString(CultureInfo.InvariantCulture));
|
|
||||||
writer.WriteLine(waypoint.LegacyReferenceValid
|
|
||||||
.ToString(CultureInfo.InvariantCulture));
|
|
||||||
WriteDouble(writer, waypoint.Position.EastWest);
|
|
||||||
WriteDouble(writer, waypoint.Position.NorthSouth);
|
|
||||||
WriteDouble(writer, waypoint.Position.Elevation);
|
|
||||||
break;
|
|
||||||
case RouteWaypointType.Jump:
|
|
||||||
WriteDouble(writer, waypoint.JumpHeadingDegrees);
|
|
||||||
writer.WriteLine(waypoint.JumpRun.ToString(CultureInfo.InvariantCulture));
|
|
||||||
string suffix = waypoint.JumpDirection switch
|
|
||||||
{
|
|
||||||
RouteJumpDirection.StrafeLeft => "4",
|
|
||||||
RouteJumpDirection.StrafeRight => "5",
|
|
||||||
_ => "3",
|
|
||||||
};
|
|
||||||
writer.WriteLine(
|
|
||||||
waypoint.JumpChargeMilliseconds.ToString(
|
|
||||||
"0.0000",
|
|
||||||
CultureInfo.InvariantCulture)
|
|
||||||
+ suffix);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Unknown waypoint type {waypoint.Type}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteDouble(TextWriter writer, double value) =>
|
|
||||||
writer.WriteLine(Convert.ToString(value, CultureInfo.InvariantCulture));
|
|
||||||
|
|
||||||
private static void ParseJump(string source, RouteWaypoint target)
|
private static void ParseJump(string source, RouteWaypoint target)
|
||||||
{
|
{
|
||||||
string value = source.Trim();
|
string value = source.Trim();
|
||||||
|
|
@ -277,7 +204,14 @@ internal static class VtankNavRouteSerializer
|
||||||
0f,
|
0f,
|
||||||
IsOutdoor: true);
|
IsOutdoor: true);
|
||||||
|
|
||||||
private static void Apply(NavigationSettings source, NavigationSettings target)
|
/// <summary>
|
||||||
|
/// Copies every field <see cref="NavigationSettings"/> owns from
|
||||||
|
/// <paramref name="source"/> into the live <paramref name="target"/>
|
||||||
|
/// instance (internal, not private, so <c>MetaEngine</c>'s embedded-
|
||||||
|
/// route consumer can reuse the exact same copy the top-level import
|
||||||
|
/// path uses instead of re-parsing already-typed data).
|
||||||
|
/// </summary>
|
||||||
|
internal static void Apply(NavigationSettings source, NavigationSettings target)
|
||||||
{
|
{
|
||||||
target.Mode = source.Mode;
|
target.Mode = source.Mode;
|
||||||
target.FollowTargetObjectId = source.FollowTargetObjectId;
|
target.FollowTargetObjectId = source.FollowTargetObjectId;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,26 @@ namespace AcDream.Plugins.MossTank;
|
||||||
/// <c>uTank2.Resources.defaultsettings.usd</c>. Order is retained because
|
/// <c>uTank2.Resources.defaultsettings.usd</c>. Order is retained because
|
||||||
/// <c>/vt opt list</c> presents the database order four entries per line.
|
/// <c>/vt opt list</c> presents the database order four entries per line.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <summary>
|
||||||
|
/// VTank's <c>uTank2.eSettingValueType</c> enum
|
||||||
|
/// (<c>refs/vtank/decompiled/uTank2/eSettingValueType.cs</c>) — the value
|
||||||
|
/// each Settings row's own <c>SettingType</c> cell declares. This is the
|
||||||
|
/// authority for which <see cref="VtankCell"/> type tag a Settings row's
|
||||||
|
/// Value cell must use; it is independent of the CLR type of the C# field
|
||||||
|
/// the live setting is stored in (e.g. every Recharge-* percentage is a
|
||||||
|
/// <c>tInt</c> row even though the live field is a <c>double</c> fraction).
|
||||||
|
/// </summary>
|
||||||
|
internal enum VtankSettingValueType
|
||||||
|
{
|
||||||
|
Bool = 1,
|
||||||
|
Double = 2,
|
||||||
|
Int = 3,
|
||||||
|
Single = 4,
|
||||||
|
String = 5,
|
||||||
|
Enum = 6,
|
||||||
|
Custom = 7,
|
||||||
|
}
|
||||||
|
|
||||||
internal static class VtankOptionCatalog
|
internal static class VtankOptionCatalog
|
||||||
{
|
{
|
||||||
internal static readonly string[] Names =
|
internal static readonly string[] Names =
|
||||||
|
|
@ -208,6 +228,151 @@ internal static class VtankOptionCatalog
|
||||||
["RechargeHandlerSet"] = MonsterValue.FromText("RechargeHandlerSet"),
|
["RechargeHandlerSet"] = MonsterValue.FromText("RechargeHandlerSet"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Exact per-row SettingType from the shipped defaultsettings.usd
|
||||||
|
// Settings table (verified 2026-09-06 against the committed
|
||||||
|
// Fixtures/vtank/defaultsettings.usd, all 137 rows).
|
||||||
|
private static readonly IReadOnlyDictionary<string, VtankSettingValueType> DeclaredTypes =
|
||||||
|
new Dictionary<string, VtankSettingValueType>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["EnableLooting"] = VtankSettingValueType.Bool,
|
||||||
|
["EnableNav"] = VtankSettingValueType.Bool,
|
||||||
|
["EnableBuffing"] = VtankSettingValueType.Bool,
|
||||||
|
["EnableCombat"] = VtankSettingValueType.Bool,
|
||||||
|
["SpellDiffExcessThreshold-Hunt"] = VtankSettingValueType.Int,
|
||||||
|
["SpellDiffExcessThreshold-Buff"] = VtankSettingValueType.Int,
|
||||||
|
["ArrowheadFletchDiffExcessThreshold"] = VtankSettingValueType.Int,
|
||||||
|
["Recharge-Norm-HitP"] = VtankSettingValueType.Int,
|
||||||
|
["Recharge-Norm-Stam"] = VtankSettingValueType.Int,
|
||||||
|
["Recharge-Norm-Mana"] = VtankSettingValueType.Int,
|
||||||
|
["Recharge-NoTarg-HitP"] = VtankSettingValueType.Int,
|
||||||
|
["Recharge-NoTarg-Stam"] = VtankSettingValueType.Int,
|
||||||
|
["Recharge-NoTarg-Mana"] = VtankSettingValueType.Int,
|
||||||
|
["Recharge-Helper-HitP"] = VtankSettingValueType.Int,
|
||||||
|
["Recharge-Helper-Stam"] = VtankSettingValueType.Int,
|
||||||
|
["Recharge-Helper-Mana"] = VtankSettingValueType.Int,
|
||||||
|
["DoHelp"] = VtankSettingValueType.Bool,
|
||||||
|
["AttackDistance"] = VtankSettingValueType.Double,
|
||||||
|
["AttackMinimumDistance"] = VtankSettingValueType.Double,
|
||||||
|
["ApproachDistance"] = VtankSettingValueType.Double,
|
||||||
|
["RingDistance"] = VtankSettingValueType.Double,
|
||||||
|
["CorpseApproachRange-Max"] = VtankSettingValueType.Double,
|
||||||
|
["CorpseApproachRange-Min"] = VtankSettingValueType.Double,
|
||||||
|
["NavCloseStopRange"] = VtankSettingValueType.Double,
|
||||||
|
["NavFarStopRange"] = VtankSettingValueType.Double,
|
||||||
|
["UsePortalDistance"] = VtankSettingValueType.Double,
|
||||||
|
["HelperDistanceHitP"] = VtankSettingValueType.Double,
|
||||||
|
["HelperDistanceStam"] = VtankSettingValueType.Double,
|
||||||
|
["HelperDistanceMana"] = VtankSettingValueType.Double,
|
||||||
|
["MinimumRingTargets"] = VtankSettingValueType.Int,
|
||||||
|
["DefaultMeleeAttackHeight"] = VtankSettingValueType.Int,
|
||||||
|
["CastDispelSelf"] = VtankSettingValueType.Bool,
|
||||||
|
["UseDispelItems"] = VtankSettingValueType.Bool,
|
||||||
|
["AutoCram"] = VtankSettingValueType.Bool,
|
||||||
|
["AutoStack"] = VtankSettingValueType.Bool,
|
||||||
|
["ReadUnknownScrolls"] = VtankSettingValueType.Bool,
|
||||||
|
["UseDispelDrum"] = VtankSettingValueType.Bool,
|
||||||
|
["SwitchWandsToDebuff"] = VtankSettingValueType.Bool,
|
||||||
|
["AutoCraftItems"] = VtankSettingValueType.Bool,
|
||||||
|
["UseHealersHeart"] = VtankSettingValueType.Bool,
|
||||||
|
["JumpOutWandCasting"] = VtankSettingValueType.Bool,
|
||||||
|
["LootAllCorpses"] = VtankSettingValueType.Bool,
|
||||||
|
["LootFellowCorpses"] = VtankSettingValueType.Bool,
|
||||||
|
["DoJiggle"] = VtankSettingValueType.Bool,
|
||||||
|
["RandomHelperBuffs"] = VtankSettingValueType.Bool,
|
||||||
|
["RandomHelperIntervalSeconds"] = VtankSettingValueType.Double,
|
||||||
|
["IdlePeaceMode"] = VtankSettingValueType.Bool,
|
||||||
|
["TargetLock"] = VtankSettingValueType.Bool,
|
||||||
|
["StopMacroOnDeath"] = VtankSettingValueType.Bool,
|
||||||
|
["UseArcs"] = VtankSettingValueType.Enum,
|
||||||
|
["ArcRange"] = VtankSettingValueType.Double,
|
||||||
|
["TargetSelectMethod"] = VtankSettingValueType.Enum,
|
||||||
|
["TargetSelectAngleRange"] = VtankSettingValueType.Double,
|
||||||
|
["IdleBuffTopoff"] = VtankSettingValueType.Bool,
|
||||||
|
["IdleBuffTopoffTimeSeconds"] = VtankSettingValueType.Int,
|
||||||
|
["RebuffTimeRemainingSeconds"] = VtankSettingValueType.Int,
|
||||||
|
["RefillWornMana"] = VtankSettingValueType.Bool,
|
||||||
|
["RefillWornMana-Item-ManaPercent"] = VtankSettingValueType.Int,
|
||||||
|
["BuffProfile-Prots"] = VtankSettingValueType.String,
|
||||||
|
["BuffProfile-Banes"] = VtankSettingValueType.String,
|
||||||
|
["BuffProfile_Prots"] = VtankSettingValueType.Enum,
|
||||||
|
["BuffProfile_Banes"] = VtankSettingValueType.Enum,
|
||||||
|
["DebuffEachFirst"] = VtankSettingValueType.Enum,
|
||||||
|
["AutoAttackPower"] = VtankSettingValueType.Bool,
|
||||||
|
["LootPriorityBoost"] = VtankSettingValueType.Bool,
|
||||||
|
["CorpseCacheTimeoutMinutes"] = VtankSettingValueType.Double,
|
||||||
|
["CorpseItemAppearanceTimeoutSeconds"] = VtankSettingValueType.Double,
|
||||||
|
["CorpseItemIDTimeoutSeconds"] = VtankSettingValueType.Double,
|
||||||
|
["DebuffSelectionMethod"] = VtankSettingValueType.Enum,
|
||||||
|
["ManaStoneLootCount"] = VtankSettingValueType.Int,
|
||||||
|
["ManaTankMinimumMana"] = VtankSettingValueType.Int,
|
||||||
|
["SplitPeas"] = VtankSettingValueType.Bool,
|
||||||
|
["SpellCompMin-Critical"] = VtankSettingValueType.Int,
|
||||||
|
["SpellCompMin-Normal"] = VtankSettingValueType.Int,
|
||||||
|
["SpellCompMin-Idle"] = VtankSettingValueType.Int,
|
||||||
|
["RechargeBoostTimeSeconds"] = VtankSettingValueType.Double,
|
||||||
|
["RechargeBoostAmount"] = VtankSettingValueType.Int,
|
||||||
|
["UseSpecialAmmo"] = VtankSettingValueType.Enum,
|
||||||
|
["OpenDoors"] = VtankSettingValueType.Bool,
|
||||||
|
["DoorIDRange"] = VtankSettingValueType.Double,
|
||||||
|
["DoorOpenRange"] = VtankSettingValueType.Double,
|
||||||
|
["DoorLockpickDiffExcessThreshold"] = VtankSettingValueType.Int,
|
||||||
|
["ManaChargesWhenOff"] = VtankSettingValueType.Bool,
|
||||||
|
["AutoFellowManagement"] = VtankSettingValueType.Bool,
|
||||||
|
["MinimumHealKitSuccessChance"] = VtankSettingValueType.Int,
|
||||||
|
["UseKitsInMagicMode"] = VtankSettingValueType.Bool,
|
||||||
|
["StaminaToHealthMultiplier"] = VtankSettingValueType.Double,
|
||||||
|
["ManaToHealthMultiplier"] = VtankSettingValueType.Double,
|
||||||
|
["NavPriorityBoost"] = VtankSettingValueType.Bool,
|
||||||
|
["DeleteGhostMonsters"] = VtankSettingValueType.Bool,
|
||||||
|
["GhostMonsterSpellAttemptCount"] = VtankSettingValueType.Int,
|
||||||
|
["WhoYouGonnaCall"] = VtankSettingValueType.Bool,
|
||||||
|
["BlacklistMonsterAttemptCount"] = VtankSettingValueType.Int,
|
||||||
|
["BlacklistMonsterTimeoutSeconds"] = VtankSettingValueType.Int,
|
||||||
|
["CombineSalvage"] = VtankSettingValueType.Bool,
|
||||||
|
["LootOnlyRareCorpses"] = VtankSettingValueType.Bool,
|
||||||
|
["DeleteGhostMonstersByHPTracker"] = VtankSettingValueType.Bool,
|
||||||
|
["GhostDeleteHPTrackerSeconds"] = VtankSettingValueType.Int,
|
||||||
|
["GoToPeaceModeToUseKits"] = VtankSettingValueType.Bool,
|
||||||
|
["UseRecklessness"] = VtankSettingValueType.Bool,
|
||||||
|
["DebuffPrecastSeconds"] = VtankSettingValueType.Int,
|
||||||
|
["ClearLevelBoostFlagOnCast"] = VtankSettingValueType.Bool,
|
||||||
|
["IdleCraftCount_HealthKits"] = VtankSettingValueType.Int,
|
||||||
|
["IdleCraftCount_StamKits"] = VtankSettingValueType.Int,
|
||||||
|
["IdleCraftCount_ManaKits"] = VtankSettingValueType.Int,
|
||||||
|
["IdleCraftCount_HealthFood"] = VtankSettingValueType.Int,
|
||||||
|
["IdleCraftCount_StamFood"] = VtankSettingValueType.Int,
|
||||||
|
["IdleCraftCount_ManaFood"] = VtankSettingValueType.Int,
|
||||||
|
["BuffCastRecast_Seconds"] = VtankSettingValueType.Int,
|
||||||
|
["BuffCastRecastReset_Seconds"] = VtankSettingValueType.Int,
|
||||||
|
["EnableMeta"] = VtankSettingValueType.Bool,
|
||||||
|
["BlacklistedSpellComps"] = VtankSettingValueType.String,
|
||||||
|
["DropToPeaceModeRetryCount"] = VtankSettingValueType.Int,
|
||||||
|
["FollowAroundCorners"] = VtankSettingValueType.Bool,
|
||||||
|
["BlacklistCorpseOpenAttemptCount"] = VtankSettingValueType.Int,
|
||||||
|
["BlacklistCorpseOpenTimeoutSeconds"] = VtankSettingValueType.Int,
|
||||||
|
["SummonPets"] = VtankSettingValueType.Bool,
|
||||||
|
["PetRangeMode"] = VtankSettingValueType.Enum,
|
||||||
|
["PetCustomRange"] = VtankSettingValueType.Double,
|
||||||
|
["PetRefillCount-Idle"] = VtankSettingValueType.Int,
|
||||||
|
["PetRefillCount-Normal"] = VtankSettingValueType.Int,
|
||||||
|
["CorpseOpenTimeoutSeconds"] = VtankSettingValueType.Double,
|
||||||
|
["PetMonsterDensity"] = VtankSettingValueType.Int,
|
||||||
|
["CorpseLootItemMaxAttempts"] = VtankSettingValueType.Int,
|
||||||
|
["FastCastBuffs"] = VtankSettingValueType.Bool,
|
||||||
|
["UseBreakableTurnTo"] = VtankSettingValueType.Bool,
|
||||||
|
["UseProjectileAwareness"] = VtankSettingValueType.Bool,
|
||||||
|
["CollisionProjectileRadius"] = VtankSettingValueType.Double,
|
||||||
|
["CollisionStepDistance"] = VtankSettingValueType.Double,
|
||||||
|
["ShowCollisionDebug"] = VtankSettingValueType.Bool,
|
||||||
|
["MaximumCollisionChecksPerTick"] = VtankSettingValueType.Int,
|
||||||
|
["SpellRangeFudge"] = VtankSettingValueType.Double,
|
||||||
|
["BuffWithUntrained-Item"] = VtankSettingValueType.Int,
|
||||||
|
["BuffWithUntrained-Creature"] = VtankSettingValueType.Int,
|
||||||
|
["BuffWithUntrained-Life"] = VtankSettingValueType.Int,
|
||||||
|
["AllowDebuffFallback"] = VtankSettingValueType.Bool,
|
||||||
|
["RechargeHandlerSet"] = VtankSettingValueType.Custom,
|
||||||
|
};
|
||||||
|
|
||||||
internal static bool IsKnown(string name) =>
|
internal static bool IsKnown(string name) =>
|
||||||
Names.Contains(name, StringComparer.OrdinalIgnoreCase);
|
Names.Contains(name, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
|
@ -218,4 +383,10 @@ internal static class VtankOptionCatalog
|
||||||
Defaults.TryGetValue(name, out MonsterValue value)
|
Defaults.TryGetValue(name, out MonsterValue value)
|
||||||
? value
|
? value
|
||||||
: MonsterValue.FromNumber(0d);
|
: MonsterValue.FromNumber(0d);
|
||||||
|
|
||||||
|
/// <summary>The <c>eSettingValueType</c> VTank's own Settings row declares for this name.</summary>
|
||||||
|
internal static VtankSettingValueType DeclaredType(string name) =>
|
||||||
|
DeclaredTypes.TryGetValue(name, out VtankSettingValueType type)
|
||||||
|
? type
|
||||||
|
: VtankSettingValueType.Double;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
404
src/AcDream.Plugins.MossTank/VtankProfileDirectory.cs
Normal file
404
src/AcDream.Plugins.MossTank/VtankProfileDirectory.cs
Normal file
|
|
@ -0,0 +1,404 @@
|
||||||
|
using AcDream.Plugin.Abstractions;
|
||||||
|
|
||||||
|
namespace AcDream.Plugins.MossTank;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implements VTank's real profile-directory naming/selection rules
|
||||||
|
/// (<c>docs/research/vtank-kb/01-settings-and-profiles.md</c> section 3):
|
||||||
|
/// the per-character auto file, the longer <c>--Name_Server_</c>
|
||||||
|
/// sub-profile prefix, and which filenames a given character can see.
|
||||||
|
///
|
||||||
|
/// The directory itself is never resolved here: every listing method reads
|
||||||
|
/// through <see cref="IPluginHost.VtankProfiles"/> (an <see cref="IPluginStorage"/>
|
||||||
|
/// the App/Headless host composes — real installed VTank's own profile
|
||||||
|
/// folder for direct interop, or a portable per-OS default under
|
||||||
|
/// <c>ApplicationPathSet.DataDirectory</c>). This class has no
|
||||||
|
/// <c>System.IO</c> dependency and no portable-default fallback of its
|
||||||
|
/// own: when the host has no opinion, <see cref="IPluginHost.VtankProfiles"/>
|
||||||
|
/// defaults to the inert <c>NoOpPluginStorage</c> (<c>IsAvailable</c>
|
||||||
|
/// false), and every listing here degrades to just its built-in entries,
|
||||||
|
/// exactly like a missing directory used to.
|
||||||
|
/// </summary>
|
||||||
|
internal static class VtankProfileDirectory
|
||||||
|
{
|
||||||
|
/// <summary>The real VTank "--" reserved-prefix marker (section 3).</summary>
|
||||||
|
internal const string HiddenPrefix = "--";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The nav-profile-only "~~" reserved prefix (section 3) — VTank's own
|
||||||
|
/// producer/purpose was not determined by the KB research pass either;
|
||||||
|
/// this only reproduces the filter.
|
||||||
|
/// </summary>
|
||||||
|
internal const string NavHiddenPrefix = "~~";
|
||||||
|
|
||||||
|
internal const string ByCharacterLabel = "[By char]";
|
||||||
|
internal const string DefaultLabel = "[Default]";
|
||||||
|
internal const string NoneLabel = "[None]";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <c>nav_</c> marker (round 3 items 3/4) that distinguishes a
|
||||||
|
/// stand-alone route <c>.af</c> from a Meta profile sharing the same
|
||||||
|
/// flat <see cref="IPluginHost.VtankProfiles"/> directory and extension
|
||||||
|
/// — metaf's own observed convention for a nav-only <c>.af</c> (see the
|
||||||
|
/// committed <c>nav_*.af</c> fixtures). MUST be checked AFTER stripping
|
||||||
|
/// <see cref="HiddenPrefix"/> when the marker is combined with it (see
|
||||||
|
/// <see cref="AutoCharacterFileName(string,string,string,string)"/>'s
|
||||||
|
/// <c>marker</c> parameter): a hidden per-character route file is named
|
||||||
|
/// <c>--nav_Name_Server.af</c> — hidden prefix FIRST, marker SECOND —
|
||||||
|
/// never <c>nav_--Name_Server.af</c>, which does not start with
|
||||||
|
/// <see cref="HiddenPrefix"/> at all and so defeats every
|
||||||
|
/// <c>StartsWith("--")</c> hidden-file check in this class.
|
||||||
|
/// </summary>
|
||||||
|
internal const string NavMarker = "nav_";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VTank's single per-character default filename
|
||||||
|
/// (<c>uTank2/PluginCore.cs:3863-3865</c>): <c>--Name_Server.ext</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static string AutoCharacterFileName(
|
||||||
|
string characterName,
|
||||||
|
string server,
|
||||||
|
string extension) =>
|
||||||
|
$"{HiddenPrefix}{characterName}_{server}.{extension.TrimStart('.')}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Overload for a per-character auto file that ALSO carries a kind
|
||||||
|
/// marker (currently only <see cref="NavMarker"/>, for
|
||||||
|
/// <see cref="MossTankRouteProfileStore"/>'s route <c>.af</c>): the
|
||||||
|
/// hidden prefix always comes first so the file stays hidden from every
|
||||||
|
/// other character's picker exactly like the unmarked overload above —
|
||||||
|
/// <c>--{marker}Name_Server.ext</c>, e.g. <c>--nav_Name_Server.af</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static string AutoCharacterFileName(
|
||||||
|
string characterName,
|
||||||
|
string server,
|
||||||
|
string extension,
|
||||||
|
string marker) =>
|
||||||
|
$"{HiddenPrefix}{marker}{characterName}_{server}.{extension.TrimStart('.')}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The longer, trailing-underscore prefix
|
||||||
|
/// (<c>uTank2/PluginCore.cs:933,3866</c>, field <c>dw</c>) that marks a
|
||||||
|
/// *named sub-profile* belonging to one character, distinct from that
|
||||||
|
/// character's single auto file above.
|
||||||
|
/// </summary>
|
||||||
|
public static string SubProfilePrefix(string characterName, string server) =>
|
||||||
|
$"{HiddenPrefix}{characterName}_{server}_";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when <paramref name="fileName"/> must be hidden from the
|
||||||
|
/// cross-character profile picker for <paramref name="characterName"/>/
|
||||||
|
/// <paramref name="server"/>: any <c>--</c>-prefixed name that is not
|
||||||
|
/// this character's own sub-profile family
|
||||||
|
/// (<c>uTank2/PluginCore.cs:7020,7072,7144</c>).
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsHiddenFromOtherCharacters(
|
||||||
|
string fileName,
|
||||||
|
string characterName,
|
||||||
|
string server) =>
|
||||||
|
fileName.StartsWith(HiddenPrefix, StringComparison.Ordinal)
|
||||||
|
&& !fileName.StartsWith(
|
||||||
|
SubProfilePrefix(characterName, server),
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The "[Char] suffix" display form for one of this character's own
|
||||||
|
/// sub-profiles (<c>uTank2/PluginCore.cs:7016-7046</c>): the
|
||||||
|
/// <c>--Name_Server_</c> prefix and the file extension are both
|
||||||
|
/// stripped. Returns <see langword="null"/> for a filename that is not
|
||||||
|
/// one of this character's sub-profiles.
|
||||||
|
/// </summary>
|
||||||
|
public static string? TryDisplayName(
|
||||||
|
string fileName,
|
||||||
|
string characterName,
|
||||||
|
string server)
|
||||||
|
{
|
||||||
|
string prefix = SubProfilePrefix(characterName, server);
|
||||||
|
if (!fileName.StartsWith(prefix, StringComparison.Ordinal))
|
||||||
|
return null;
|
||||||
|
string withoutPrefix = fileName[prefix.Length..];
|
||||||
|
int dot = withoutPrefix.LastIndexOf('.');
|
||||||
|
string suffix = dot >= 0 ? withoutPrefix[..dot] : withoutPrefix;
|
||||||
|
return suffix.Length == 0 ? null : $"[Char] {suffix}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One entry in a profile picker: the real on-disk file name, and the
|
||||||
|
/// label VTank would show for it.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct ProfileEntry(string FileName, string DisplayName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VTank's settings-profile list (<c>a0()</c>,
|
||||||
|
/// <c>uTank2/PluginCore.cs:7057-7115</c>): seeds
|
||||||
|
/// <see cref="DefaultLabel"/> and <see cref="ByCharacterLabel"/> first,
|
||||||
|
/// then every non-<c>--</c> <c>.usd</c> file, plus every one of this
|
||||||
|
/// character's own <c>--Name_Server_*</c> sub-profiles shown as
|
||||||
|
/// <c>[Char] suffix</c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mineOnly">
|
||||||
|
/// The "Mine only" checkbox (<c>cSettingsShowAll</c>/field <c>a9</c>).
|
||||||
|
/// VTank's own predicate (<c>uTank2/PluginCore.cs:7020-7024</c>) is NOT
|
||||||
|
/// "hide every shared file" — a shared (non-sub-profile) file is hidden
|
||||||
|
/// only when it is neither unchecked NOR the currently selected file,
|
||||||
|
/// so the profile actually in use never vanishes out from under the
|
||||||
|
/// user just because they ticked the box afterward.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="currentFileName">
|
||||||
|
/// The file name currently assigned to <paramref name="characterName"/>
|
||||||
|
/// (if any), exempted from <paramref name="mineOnly"/> filtering per the
|
||||||
|
/// rule above. Comparison is ordinal (VTank's own file names are
|
||||||
|
/// case-sensitive on the filesystems it ships for).
|
||||||
|
/// </param>
|
||||||
|
public static IReadOnlyList<ProfileEntry> ListSettingsProfiles(
|
||||||
|
IPluginStorage storage,
|
||||||
|
string characterName,
|
||||||
|
string server,
|
||||||
|
bool mineOnly,
|
||||||
|
string? currentFileName = null)
|
||||||
|
{
|
||||||
|
var entries = new List<ProfileEntry>
|
||||||
|
{
|
||||||
|
new(string.Empty, DefaultLabel),
|
||||||
|
new(string.Empty, ByCharacterLabel),
|
||||||
|
};
|
||||||
|
foreach (string fileName in EnumerateFileNames(storage, ".usd"))
|
||||||
|
{
|
||||||
|
string? subProfileDisplay = TryDisplayName(fileName, characterName, server);
|
||||||
|
if (subProfileDisplay is not null)
|
||||||
|
{
|
||||||
|
entries.Add(new ProfileEntry(fileName, subProfileDisplay));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (fileName.StartsWith(HiddenPrefix, StringComparison.Ordinal))
|
||||||
|
continue; // someone else's --Name_Server(.usd|_*) family.
|
||||||
|
if (mineOnly
|
||||||
|
&& !fileName.Equals(currentFileName, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
entries.Add(new ProfileEntry(fileName, fileName));
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VTank's navigation-profile list (<c>l()</c>,
|
||||||
|
/// <c>uTank2/PluginCore.cs:7156-7185</c>): seeds
|
||||||
|
/// <see cref="NoneLabel"/>/<see cref="ByCharacterLabel"/>, then every
|
||||||
|
/// <see cref="NavMarker"/>-marked <c>.af</c> file that starts with
|
||||||
|
/// neither <c>--</c> nor <c>~~</c>. Round 3 item 4: a Meta profile and a
|
||||||
|
/// route share this same flat, single-extension directory — without
|
||||||
|
/// the marker check this returned every Meta <c>.af</c> too.
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<ProfileEntry> ListNavigationProfiles(IPluginStorage storage)
|
||||||
|
{
|
||||||
|
var entries = new List<ProfileEntry>
|
||||||
|
{
|
||||||
|
new(string.Empty, NoneLabel),
|
||||||
|
new(string.Empty, ByCharacterLabel),
|
||||||
|
};
|
||||||
|
foreach (string fileName in EnumerateFileNames(storage, ".af"))
|
||||||
|
{
|
||||||
|
if (fileName.StartsWith(HiddenPrefix, StringComparison.Ordinal)
|
||||||
|
|| fileName.StartsWith(NavHiddenPrefix, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!fileName.StartsWith(NavMarker, StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
entries.Add(new ProfileEntry(fileName, fileName));
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VTank's meta-profile list (<c>ac()</c>,
|
||||||
|
/// <c>uTank2/PluginCore.cs:7187+</c>): seeds
|
||||||
|
/// <see cref="NoneLabel"/>/<see cref="ByCharacterLabel"/>, then every
|
||||||
|
/// non-<c>--</c>, non-<see cref="NavMarker"/>-marked file (a meta and a
|
||||||
|
/// nav profile share the same directory and extension here —
|
||||||
|
/// <c>.af</c> — so the marker is what VTank's own separate
|
||||||
|
/// <c>.met</c>/<c>.nav</c> extensions used to provide; round 3 item 4:
|
||||||
|
/// without excluding <see cref="NavMarker"/> files this returned every
|
||||||
|
/// route <c>.af</c> too).
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<ProfileEntry> ListMetaProfiles(IPluginStorage storage)
|
||||||
|
{
|
||||||
|
var entries = new List<ProfileEntry>
|
||||||
|
{
|
||||||
|
new(string.Empty, NoneLabel),
|
||||||
|
new(string.Empty, ByCharacterLabel),
|
||||||
|
};
|
||||||
|
foreach (string fileName in EnumerateFileNames(storage, ".af"))
|
||||||
|
{
|
||||||
|
if (fileName.StartsWith(HiddenPrefix, StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
if (fileName.StartsWith(NavMarker, StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
entries.Add(new ProfileEntry(fileName, fileName));
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VTank's real loot-profile list (<c>aa()</c>,
|
||||||
|
/// <c>uTank2/PluginCore.cs:7127-7154</c>) seeds ONLY
|
||||||
|
/// <see cref="NoneLabel"/> — retail has no per-character auto loot
|
||||||
|
/// file and no "mine only" filter for loot at all (docs/research/vtank-kb/01-settings-and-profiles.md
|
||||||
|
/// section 3: "the loot default has no equivalent auto-name; loot
|
||||||
|
/// profiles default to none") — then every non-<c>--</c> file across
|
||||||
|
/// whatever loot-profile extension family that build understands.
|
||||||
|
/// Round 3 item 9: MossTank keeps its own established
|
||||||
|
/// <see cref="ByCharacterLabel"/> convention (already load-bearing for
|
||||||
|
/// Settings/Nav/Meta) as a SECOND seed entry for internal consistency
|
||||||
|
/// across all four stores — a recorded adaptation (register), not a
|
||||||
|
/// retail behavior — while still seeding retail's own
|
||||||
|
/// <see cref="NoneLabel"/> first.
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<ProfileEntry> ListLootProfiles(IPluginStorage storage)
|
||||||
|
{
|
||||||
|
var entries = new List<ProfileEntry>
|
||||||
|
{
|
||||||
|
new(string.Empty, NoneLabel),
|
||||||
|
new(string.Empty, ByCharacterLabel),
|
||||||
|
};
|
||||||
|
foreach (string fileName in EnumerateFileNames(storage, ".utl"))
|
||||||
|
{
|
||||||
|
if (fileName.StartsWith(HiddenPrefix, StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
entries.Add(new ProfileEntry(fileName, fileName));
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The per-character spell-tracking cache filename (<c>dm</c> class,
|
||||||
|
/// <c>refs/vtank/decompiled/dm.cs:391</c>): always
|
||||||
|
/// <c>CharacterName_Server.ast</c> — no <c>"--"</c> prefix (unlike the
|
||||||
|
/// auto settings/nav/meta files), not user-selectable, and note the
|
||||||
|
/// concatenation ORDER (Name first, then Server) is the reverse of
|
||||||
|
/// <see cref="CdfFileName"/>'s Server-then-Name order.
|
||||||
|
/// </summary>
|
||||||
|
public static string AstFileName(string characterName, string server) =>
|
||||||
|
$"{characterName}_{server}.ast";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The per-character binding file's own name (<c>da</c> class,
|
||||||
|
/// <c>refs/vtank/decompiled/da.cs:110</c>): always
|
||||||
|
/// <c>Server_CharacterName.cdf</c> — Server first, then Name (the
|
||||||
|
/// reverse of <see cref="AstFileName"/> and of the auto
|
||||||
|
/// <c>--Name_Server.usd</c> naming).
|
||||||
|
/// </summary>
|
||||||
|
public static string CdfFileName(string characterName, string server) =>
|
||||||
|
$"{server}_{characterName}.cdf";
|
||||||
|
|
||||||
|
/// <summary>The literal version header a valid <c>.cdf</c> starts with (<c>da.cs:15</c>).</summary>
|
||||||
|
internal const string CdfHeader = "uTank2 CDF 1.0";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One character's currently-assigned profile filenames, read from its
|
||||||
|
/// <c>.cdf</c> (<c>da.e()</c>, <c>da.cs:105-154</c>). <c>Meta</c> is
|
||||||
|
/// <see langword="null"/> when the file is short (line 5 is present
|
||||||
|
/// only when the stream isn't already at EOF, <c>da.cs:125-128</c> —
|
||||||
|
/// an older <c>.cdf</c> predating meta support has no line 5 at all).
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct VtankCharacterBinding(
|
||||||
|
string SettingsFileName,
|
||||||
|
string LootFileName,
|
||||||
|
string NavFileName,
|
||||||
|
string? MetaFileName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads <paramref name="characterName"/>/<paramref name="server"/>'s
|
||||||
|
/// <c>.cdf</c> through <paramref name="storage"/>. Returns
|
||||||
|
/// <see langword="null"/> when the storage is unavailable, the file is
|
||||||
|
/// missing, or the file does not start with <see cref="CdfHeader"/> —
|
||||||
|
/// real VTank treats a version mismatch identically to a missing file
|
||||||
|
/// and falls back to that character's auto-created per-character
|
||||||
|
/// defaults (<c>da.cs:113-121</c>), which this method leaves entirely
|
||||||
|
/// to the caller rather than fabricating a default binding itself. The
|
||||||
|
/// legacy <c>.uts</c>→<c>.usd</c> settings-filename rewrite
|
||||||
|
/// (<c>da.cs:130-141</c>) is applied here so callers never see a
|
||||||
|
/// <c>.uts</c> name.
|
||||||
|
/// </summary>
|
||||||
|
public static VtankCharacterBinding? TryReadCharacterBinding(
|
||||||
|
IPluginStorage storage,
|
||||||
|
string characterName,
|
||||||
|
string server)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(storage);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(characterName);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(server);
|
||||||
|
if (!storage.IsAvailable)
|
||||||
|
return null;
|
||||||
|
string? text = storage.ReadText(CdfFileName(characterName, server));
|
||||||
|
if (text is null)
|
||||||
|
return null;
|
||||||
|
string[] lines = text
|
||||||
|
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||||
|
.Split('\n');
|
||||||
|
if (lines.Length < 4 || !string.Equals(lines[0], CdfHeader, StringComparison.Ordinal))
|
||||||
|
return null;
|
||||||
|
string settings = lines[1].EndsWith(".uts", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? string.Concat(lines[1].AsSpan(0, lines[1].Length - 4), ".usd")
|
||||||
|
: lines[1];
|
||||||
|
string? meta = lines.Length >= 5 && lines[4].Length > 0 ? lines[4] : null;
|
||||||
|
return new VtankCharacterBinding(settings, lines[2], lines[3], meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes <paramref name="binding"/> as <paramref name="characterName"/>/
|
||||||
|
/// <paramref name="server"/>'s <c>.cdf</c> (<c>da.q()</c>,
|
||||||
|
/// <c>refs/vtank/decompiled/da.cs:156-164</c>): the version header, then
|
||||||
|
/// settings/loot/nav on lines 2-4, then the meta filename on line 5 only
|
||||||
|
/// when <see cref="VtankCharacterBinding.MetaFileName"/> is non-empty —
|
||||||
|
/// matching <see cref="TryReadCharacterBinding"/>'s own line-5-is-optional
|
||||||
|
/// parsing exactly. No-ops when the storage is unavailable.
|
||||||
|
/// </summary>
|
||||||
|
public static void WriteCharacterBinding(
|
||||||
|
IPluginStorage storage,
|
||||||
|
string characterName,
|
||||||
|
string server,
|
||||||
|
VtankCharacterBinding binding)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(storage);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(characterName);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(server);
|
||||||
|
if (!storage.IsAvailable)
|
||||||
|
return;
|
||||||
|
var lines = new List<string>
|
||||||
|
{
|
||||||
|
CdfHeader,
|
||||||
|
binding.SettingsFileName,
|
||||||
|
binding.LootFileName,
|
||||||
|
binding.NavFileName,
|
||||||
|
};
|
||||||
|
if (!string.IsNullOrEmpty(binding.MetaFileName))
|
||||||
|
lines.Add(binding.MetaFileName);
|
||||||
|
storage.WriteText(
|
||||||
|
CdfFileName(characterName, server),
|
||||||
|
string.Join("\r\n", lines) + "\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lists root-level file names matching <paramref name="extension"/>
|
||||||
|
/// through <see cref="IPluginStorage.List"/> alone — no
|
||||||
|
/// <c>System.IO</c>. VTank's own profile directory is flat, so a key
|
||||||
|
/// containing '/' (meaning it lives in some deeper storage
|
||||||
|
/// implementation's sub-directory) is not one of these files and is
|
||||||
|
/// skipped rather than surfaced as a bogus profile name.
|
||||||
|
/// </summary>
|
||||||
|
private static IEnumerable<string> EnumerateFileNames(IPluginStorage storage, string extension)
|
||||||
|
{
|
||||||
|
if (!storage.IsAvailable)
|
||||||
|
yield break;
|
||||||
|
foreach (string key in storage.List(string.Empty)
|
||||||
|
.Where(key => !key.Contains('/', StringComparison.Ordinal))
|
||||||
|
.Where(key => key.EndsWith(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.OrderBy(static key => key, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
yield return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
542
src/AcDream.Plugins.MossTank/VtankSettingsProfileSerializer.cs
Normal file
542
src/AcDream.Plugins.MossTank/VtankSettingsProfileSerializer.cs
Normal file
|
|
@ -0,0 +1,542 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using AcDream.Plugin.Abstractions;
|
||||||
|
|
||||||
|
namespace AcDream.Plugins.MossTank;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads/saves VTank's real <c>.usd</c> settings profile (the self-describing
|
||||||
|
/// <c>gy</c>/<c>bd</c>/<c>y</c> grammar in <see cref="VtankDatabase"/>),
|
||||||
|
/// mapping every one of the 137 <c>Settings</c> table rows
|
||||||
|
/// (<see cref="VtankOptionCatalog.Names"/>) onto the live
|
||||||
|
/// <see cref="CombatSettings"/>/<see cref="BuffSettings"/>/
|
||||||
|
/// <see cref="VitalSettings"/>/<see cref="InventorySettings"/>/
|
||||||
|
/// <see cref="NavigationSettings"/> fields by VTank name.
|
||||||
|
///
|
||||||
|
/// The value transforms (unit scaling, enum offsets) are the exact ones
|
||||||
|
/// already verified in <c>MossTankPanel.SetMetaOption</c>/<c>GetMetaOption</c>
|
||||||
|
/// (<c>MossTankPanel.cs:2442-3377</c>) against the decompiled VTank source
|
||||||
|
/// per <c>docs/research/vtank-kb/01-settings-and-profiles.md</c> — this
|
||||||
|
/// class duplicates those transforms rather than calling into
|
||||||
|
/// <c>MossTankPanel</c> because the panel's methods operate on a live
|
||||||
|
/// session (chat, expression state) that a file-format serializer must not
|
||||||
|
/// depend on.
|
||||||
|
///
|
||||||
|
/// Every table other than <c>Settings</c> (MyMonsters, GemFoodItems,
|
||||||
|
/// ExtraBuffSpells, AntiExtraBuffSpells, ItemUseSpecifiers,
|
||||||
|
/// SettingsCategories, SettingsEnumInfo, AssistItems, BuffedItems, …) is
|
||||||
|
/// preserved byte-for-byte: <see cref="Save"/> mutates the exact
|
||||||
|
/// <see cref="VtankDatabase"/> object <see cref="Load"/> returned, and only
|
||||||
|
/// replaces a <c>Settings</c> row's Value cell when the live setting's
|
||||||
|
/// current value differs from what was parsed (a value-equality compare,
|
||||||
|
/// not a text compare, so an untouched profile round-trips byte-for-byte
|
||||||
|
/// even if VTank's own float formatting differs subtly from ours).
|
||||||
|
/// </summary>
|
||||||
|
internal static class VtankSettingsProfileSerializer
|
||||||
|
{
|
||||||
|
private const string SettingsTable = "Settings";
|
||||||
|
|
||||||
|
internal sealed class AllSettings
|
||||||
|
{
|
||||||
|
public required CombatSettings Combat { get; init; }
|
||||||
|
public required BuffSettings Buffs { get; init; }
|
||||||
|
public required VitalSettings Vitals { get; init; }
|
||||||
|
public required InventorySettings Inventory { get; init; }
|
||||||
|
public required NavigationSettings Navigation { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses <paramref name="text"/> and applies every recognized Settings
|
||||||
|
/// row onto <paramref name="target"/>. Returns the parsed
|
||||||
|
/// <see cref="VtankDatabase"/> so a later <see cref="Save"/> can
|
||||||
|
/// preserve every other table (and every unrecognized Settings row)
|
||||||
|
/// byte-for-byte. Throws <see cref="FormatException"/> with a
|
||||||
|
/// <c>line N:</c>-prefixed message on the first structural error.
|
||||||
|
/// </summary>
|
||||||
|
public static VtankDatabase Load(string text, AllSettings target)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(target);
|
||||||
|
VtankDatabase database = VtankDatabase.Parse(text);
|
||||||
|
VtankTable? settings = database.Find(SettingsTable);
|
||||||
|
if (settings is null)
|
||||||
|
throw new FormatException("missing required 'Settings' table.");
|
||||||
|
int nameColumn = settings.ColumnIndex("Setting");
|
||||||
|
int valueColumn = settings.ColumnIndex("Value");
|
||||||
|
if (nameColumn < 0 || valueColumn < 0)
|
||||||
|
throw new FormatException("'Settings' table is missing Setting/Value columns.");
|
||||||
|
|
||||||
|
foreach (VtankRow row in settings.Rows)
|
||||||
|
{
|
||||||
|
string name = row.Cells[nameColumn].AsString();
|
||||||
|
Apply(name, row.Cells[valueColumn], target);
|
||||||
|
}
|
||||||
|
return database;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Regenerates the <c>Settings</c> table's Value cells from
|
||||||
|
/// <paramref name="source"/>'s current values (skipping any row whose
|
||||||
|
/// value is unchanged from what <paramref name="document"/> already
|
||||||
|
/// holds) and re-renders the whole database. Every other table, and any
|
||||||
|
/// Settings row this serializer does not recognize, is untouched.
|
||||||
|
/// </summary>
|
||||||
|
public static string Save(VtankDatabase document, AllSettings source)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(document);
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
VtankTable? settings = document.Find(SettingsTable);
|
||||||
|
if (settings is null)
|
||||||
|
throw new FormatException("missing required 'Settings' table.");
|
||||||
|
int nameColumn = settings.ColumnIndex("Setting");
|
||||||
|
int valueColumn = settings.ColumnIndex("Value");
|
||||||
|
foreach (VtankRow row in settings.Rows)
|
||||||
|
{
|
||||||
|
string name = row.Cells[nameColumn].AsString();
|
||||||
|
VtankCell? captured = Capture(name, source);
|
||||||
|
if (captured is null)
|
||||||
|
continue;
|
||||||
|
VtankCell current = row.Cells[valueColumn];
|
||||||
|
if (ValuesEqual(current, captured))
|
||||||
|
continue;
|
||||||
|
row.Cells[valueColumn] = captured;
|
||||||
|
}
|
||||||
|
return document.Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a brand-new database seeded from VTank's own shipped
|
||||||
|
/// <c>defaultsettings.usd</c> (<see cref="VtankDefaultSettingsDatabase"/>)
|
||||||
|
/// — every table (MyMonsters, GemFoodItems, ExtraBuffSpells,
|
||||||
|
/// AntiExtraBuffSpells, ItemUseSpecifiers, SettingsCategories,
|
||||||
|
/// SettingsEnumInfo, AssistItems, BuffedItems, RechargeHandlerSet, …),
|
||||||
|
/// not a hand-typed 4-column Settings-only replica: a prior version of
|
||||||
|
/// this method built only the Settings table, with an empty
|
||||||
|
/// Description and a hardcoded SettingType of 1 (Bool) for every row
|
||||||
|
/// regardless of the setting's real declared type. <see cref="Save"/>
|
||||||
|
/// then rewrites every Settings row's Value cell from
|
||||||
|
/// <paramref name="source"/>'s current values, exactly as it would for
|
||||||
|
/// an existing profile being re-saved untouched.
|
||||||
|
/// </summary>
|
||||||
|
public static VtankDatabase CreateNew(AllSettings source)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
VtankDatabase database = VtankDefaultSettingsDatabase.Parse();
|
||||||
|
_ = Save(database, source);
|
||||||
|
return database;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a live numeric setting value to the exact
|
||||||
|
/// <see cref="VtankCell"/> shape its declared <c>SettingType</c>
|
||||||
|
/// requires — the fix for the fidelity defect where 17 <c>tInt</c>
|
||||||
|
/// settings were written as <c>d</c> and 3 <c>tSingle</c> settings were
|
||||||
|
/// written as <c>f</c> (VTank's <c>gy.e</c>/<c>gy.f</c> unbox helpers
|
||||||
|
/// throw <see cref="InvalidCastException"/> on a tag/CLR-type mismatch).
|
||||||
|
/// The tag is driven by <see cref="VtankOptionCatalog.DeclaredType"/>,
|
||||||
|
/// never by the CLR type of the C# field the caller happens to store
|
||||||
|
/// the value in.
|
||||||
|
/// </summary>
|
||||||
|
private static VtankCell Num(string name, double value) =>
|
||||||
|
VtankOptionCatalog.DeclaredType(name) switch
|
||||||
|
{
|
||||||
|
VtankSettingValueType.Bool => VtankCell.Bool(value != 0d),
|
||||||
|
VtankSettingValueType.Int or VtankSettingValueType.Enum =>
|
||||||
|
VtankCell.Int((int)Math.Round(value, MidpointRounding.AwayFromZero)),
|
||||||
|
VtankSettingValueType.Single => VtankCell.Float((float)value),
|
||||||
|
VtankSettingValueType.String => VtankCell.String(value.ToString(CultureInfo.InvariantCulture)),
|
||||||
|
_ => VtankCell.Double(value),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exact compare on the cell's own type, with no tolerance anywhere —
|
||||||
|
/// a <see cref="Capture"/> tag mismatch is a real bug, not something to
|
||||||
|
/// paper over. Item I (slice-1 fix round) removed the one narrow
|
||||||
|
/// exception this used to carry: every <c>tDouble</c>-declared distance
|
||||||
|
/// setting (AttackDistance, ArcRange, …) that used to round-trip
|
||||||
|
/// through a <c>float</c>-typed live field (CombatSettings.MaximumRange
|
||||||
|
/// etc., costing precision below float's ~7-digit guarantee) now stores
|
||||||
|
/// that value as a real <c>double</c> — the field widened to match its
|
||||||
|
/// declared type instead of the compare being loosened to match the
|
||||||
|
/// field. Physics/combat consumers that want a <c>float</c> cast it at
|
||||||
|
/// their own use site.
|
||||||
|
/// </summary>
|
||||||
|
private static bool ValuesEqual(VtankCell a, VtankCell b)
|
||||||
|
{
|
||||||
|
if (a.Tag != b.Tag)
|
||||||
|
return false;
|
||||||
|
return a.Tag switch
|
||||||
|
{
|
||||||
|
"b" => a.AsBool() == b.AsBool(),
|
||||||
|
"s" => a.AsString() == b.AsString(),
|
||||||
|
"i" => a.AsInt() == b.AsInt(),
|
||||||
|
"u" => a.AsUInt() == b.AsUInt(),
|
||||||
|
"d" or "f" => a.AsDouble() == b.AsDouble(),
|
||||||
|
_ => ReferenceEquals(a, b),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Apply: .usd cell -> live setting.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>Internal (not private) so the profile store can seed live settings directly from a parsed database (fresh/default profiles, migration).</summary>
|
||||||
|
internal static void Apply(string rawName, VtankCell cell, AllSettings s)
|
||||||
|
{
|
||||||
|
if (!VtankOptionCatalog.IsKnown(rawName))
|
||||||
|
return;
|
||||||
|
string name = VtankOptionCatalog.Canonical(rawName).ToLowerInvariant();
|
||||||
|
CombatSettings c = s.Combat;
|
||||||
|
BuffSettings b = s.Buffs;
|
||||||
|
VitalSettings v = s.Vitals;
|
||||||
|
InventorySettings i = s.Inventory;
|
||||||
|
NavigationSettings n = s.Navigation;
|
||||||
|
switch (name)
|
||||||
|
{
|
||||||
|
case "enablelooting": i.Loot.Enabled = cell.AsBool(); break;
|
||||||
|
case "enablenav": n.Enabled = cell.AsBool(); break;
|
||||||
|
case "enablebuffing": b.Enabled = cell.AsBool(); break;
|
||||||
|
case "enablecombat": c.Enabled = cell.AsBool(); break;
|
||||||
|
case "spelldiffexcessthreshold-hunt": c.HuntSkillExcessOverDifficulty = cell.AsInt(); break;
|
||||||
|
case "spelldiffexcessthreshold-buff": b.SkillExcessOverDifficulty = cell.AsInt(); break;
|
||||||
|
case "arrowheadfletchdiffexcessthreshold": i.ArrowheadFletchDifficultyExcess = cell.AsInt(); break;
|
||||||
|
case "recharge-norm-hitp": v.NormalHealth = cell.AsDouble() / 100d; break;
|
||||||
|
case "recharge-norm-stam": v.NormalStamina = cell.AsDouble() / 100d; break;
|
||||||
|
case "recharge-norm-mana": v.NormalMana = cell.AsDouble() / 100d; break;
|
||||||
|
case "recharge-notarg-hitp": v.NoTargetHealth = cell.AsDouble() / 100d; break;
|
||||||
|
case "recharge-notarg-stam": v.NoTargetStamina = cell.AsDouble() / 100d; break;
|
||||||
|
case "recharge-notarg-mana": v.NoTargetMana = cell.AsDouble() / 100d; break;
|
||||||
|
case "recharge-helper-hitp": v.HelperHealth = cell.AsDouble() / 100d; break;
|
||||||
|
case "recharge-helper-stam": v.HelperStamina = cell.AsDouble() / 100d; break;
|
||||||
|
case "recharge-helper-mana": v.HelperMana = cell.AsDouble() / 100d; break;
|
||||||
|
case "dohelp": v.HelpOthers = cell.AsBool(); break;
|
||||||
|
case "attackdistance": c.MaximumRange = cell.AsDouble() * 240d; break;
|
||||||
|
case "attackminimumdistance": c.MinimumRange = cell.AsDouble() * 240d; break;
|
||||||
|
case "approachdistance": c.ApproachDistance = cell.AsDouble() * 240d; break;
|
||||||
|
case "ringdistance": c.RingDistance = cell.AsDouble() * 240d; break;
|
||||||
|
case "corpseapproachrange-max": i.Loot.CorpseApproachRange = cell.AsDouble() * 240d; break;
|
||||||
|
case "corpseapproachrange-min": i.Loot.CorpseMinimumApproachRange = cell.AsDouble() * 240d; break;
|
||||||
|
case "navclosestoprange": n.MinimumDistanceMeters = cell.AsDouble() * 240d; break;
|
||||||
|
case "navfarstoprange": n.MaximumDistanceMeters = cell.AsDouble() * 240d; break;
|
||||||
|
case "useportaldistance": n.PortalUseDistanceMeters = cell.AsDouble() * 240d; break;
|
||||||
|
case "helperdistancehitp": v.HelperHealthDistance = cell.AsDouble() * 240d; break;
|
||||||
|
case "helperdistancestam": v.HelperStaminaDistance = cell.AsDouble() * 240d; break;
|
||||||
|
case "helperdistancemana": v.HelperManaDistance = cell.AsDouble() * 240d; break;
|
||||||
|
case "minimumringtargets": c.MinimumRingTargets = cell.AsInt(); break;
|
||||||
|
case "defaultmeleeattackheight": c.AttackHeight = (PluginAttackHeight)cell.AsInt(); break;
|
||||||
|
case "castdispelself": v.CastDispelSelf = cell.AsBool(); break;
|
||||||
|
case "usedispelitems": v.UseDispelItems = cell.AsBool(); break;
|
||||||
|
case "autocram": i.AutoCram = cell.AsBool(); break;
|
||||||
|
case "autostack": i.AutoStack = cell.AsBool(); break;
|
||||||
|
case "readunknownscrolls": i.Loot.ReadUnknownScrolls = cell.AsBool(); break;
|
||||||
|
case "usedispeldrum": v.UseDispelDrum = cell.AsBool(); break;
|
||||||
|
case "switchwandstodebuff": c.SwitchWandsToDebuff = cell.AsBool(); break;
|
||||||
|
case "autocraftitems": i.AutoCraftItems = cell.AsBool(); break;
|
||||||
|
case "usehealersheart": v.UseHealersHeart = cell.AsBool(); break;
|
||||||
|
case "jumpoutwandcasting": c.JumpOutWandCasting = cell.AsBool(); break;
|
||||||
|
case "lootallcorpses": i.Loot.LootAllCorpses = cell.AsBool(); break;
|
||||||
|
case "lootfellowcorpses": i.Loot.LootFellowCorpses = cell.AsBool(); break;
|
||||||
|
case "dojiggle": c.DoJiggle = cell.AsBool(); break;
|
||||||
|
case "randomhelperbuffs": b.RandomHelperBuffs = cell.AsBool(); break;
|
||||||
|
case "randomhelperintervalseconds": b.RandomHelperIntervalSeconds = cell.AsDouble(); break;
|
||||||
|
case "idlepeacemode": c.IdlePeaceMode = cell.AsBool(); break;
|
||||||
|
case "targetlock": c.TargetLock = cell.AsBool(); break;
|
||||||
|
case "stopmacroondeath": c.StopMacroOnDeath = cell.AsBool(); break;
|
||||||
|
case "usearcs": c.UseArcs = (UseArcsMode)cell.AsInt(); break;
|
||||||
|
case "arcrange": c.ArcRange = cell.AsDouble() * 240d; break;
|
||||||
|
case "targetselectmethod": c.SelectionMethod = (TargetSelectionMethod)(cell.AsInt() - 1); break;
|
||||||
|
case "targetselectanglerange": c.TargetSelectAngleRange = cell.AsDouble() * 240d; break;
|
||||||
|
case "idlebufftopoff": b.IdleBuffTopoff = cell.AsBool(); break;
|
||||||
|
case "idlebufftopofftimeseconds": b.IdleBuffTopoffSeconds = cell.AsDouble(); break;
|
||||||
|
case "rebufftimeremainingseconds": b.RebuffWhenUnderSeconds = cell.AsDouble(); break;
|
||||||
|
case "refillwornmana": i.RefillWornMana = cell.AsBool(); break;
|
||||||
|
case "refillwornmana-item-manapercent": i.RefillWornManaPercent = cell.AsInt(); break;
|
||||||
|
case "buffprofile-prots": b.ProtectionElements = cell.AsString(); break;
|
||||||
|
case "buffprofile-banes": b.BaneElements = cell.AsString(); break;
|
||||||
|
case "buffprofile_prots": b.ProtectionProfileMode = cell.AsInt(); break;
|
||||||
|
case "buffprofile_banes": b.BaneProfileMode = cell.AsInt(); break;
|
||||||
|
case "debuffeachfirst": c.DebuffEachFirst = (DebuffEachFirst)cell.AsInt(); break;
|
||||||
|
case "autoattackpower": c.AutoAttackPower = cell.AsBool(); break;
|
||||||
|
case "lootpriorityboost": i.Loot.PriorityBoost = cell.AsBool(); break;
|
||||||
|
case "corpsecachetimeoutminutes": i.Loot.CorpseCacheTimeoutMinutes = cell.AsDouble(); break;
|
||||||
|
case "corpseitemappearancetimeoutseconds": i.Loot.CorpseItemAppearanceTimeoutSeconds = cell.AsDouble(); break;
|
||||||
|
case "corpseitemidtimeoutseconds": i.Loot.CorpseItemIdentifyTimeoutSeconds = cell.AsDouble(); break;
|
||||||
|
case "debuffselectionmethod": c.DebuffSelectionMethod = (DebuffSelectionMethod)cell.AsInt(); break;
|
||||||
|
case "manastonelootcount": i.Loot.ManaStoneLootCount = cell.AsInt(); break;
|
||||||
|
case "manatankminimummana": i.Loot.ManaTankMinimumMana = cell.AsInt(); break;
|
||||||
|
case "splitpeas": i.SplitPeas = cell.AsBool(); break;
|
||||||
|
case "spellcompmin-critical": i.CriticalComponentMinimum = cell.AsInt(); break;
|
||||||
|
case "spellcompmin-normal": i.NormalComponentMinimum = cell.AsInt(); break;
|
||||||
|
case "spellcompmin-idle": i.IdleComponentMinimum = cell.AsInt(); break;
|
||||||
|
case "rechargeboosttimeseconds": v.RechargeBoostTimeSeconds = cell.AsDouble(); break;
|
||||||
|
case "rechargeboostamount": v.RechargeBoostAmount = cell.AsInt(); break;
|
||||||
|
case "usespecialammo": c.UseSpecialAmmo = cell.AsInt(); break;
|
||||||
|
case "opendoors": n.OpenDoors = cell.AsBool(); break;
|
||||||
|
case "dooridrange": n.DoorIdentifyRangeMeters = cell.AsDouble() * 240d; break;
|
||||||
|
case "dooropenrange": n.DoorOpenRangeMeters = cell.AsDouble() * 240d; break;
|
||||||
|
case "doorlockpickdiffexcessthreshold": n.DoorLockpickExcessThreshold = cell.AsInt(); break;
|
||||||
|
case "manachargeswhenoff": i.ManaChargesWhenOff = cell.AsBool(); break;
|
||||||
|
case "autofellowmanagement": c.AutoFellowManagement = cell.AsBool(); break;
|
||||||
|
case "minimumhealkitsuccesschance": v.MinimumHealKitSuccessChance = cell.AsInt(); break;
|
||||||
|
case "usekitsinmagicmode": v.UseKitsInMagicMode = cell.AsBool(); break;
|
||||||
|
case "staminatohealthmultiplier": v.StaminaToHealthMultiplier = cell.AsDouble(); break;
|
||||||
|
case "manatohealthmultiplier": v.ManaToHealthMultiplier = cell.AsDouble(); break;
|
||||||
|
case "navpriorityboost": n.Priority = cell.AsBool(); break;
|
||||||
|
case "deleteghostmonsters": c.DeleteGhostMonsters = cell.AsBool(); break;
|
||||||
|
case "ghostmonsterspellattemptcount": c.GhostMonsterSpellAttemptCount = cell.AsInt(); break;
|
||||||
|
case "whoyougonnacall": c.WhoYouGonnaCall = cell.AsBool(); break;
|
||||||
|
case "blacklistmonsterattemptcount": c.BlacklistMonsterAttemptCount = cell.AsInt(); break;
|
||||||
|
case "blacklistmonstertimeoutseconds": c.BlacklistMonsterTimeoutSeconds = cell.AsDouble(); break;
|
||||||
|
case "combinesalvage": i.Loot.CombineSalvage = cell.AsBool(); break;
|
||||||
|
case "lootonlyrarecorpses": i.Loot.LootOnlyRareCorpses = cell.AsBool(); break;
|
||||||
|
case "deleteghostmonstersbyhptracker": c.DeleteGhostMonstersByHealthTracker = cell.AsBool(); break;
|
||||||
|
case "ghostdeletehptrackerseconds": c.GhostDeleteHealthTrackerSeconds = cell.AsDouble(); break;
|
||||||
|
case "gotopeacemodetousekits": v.GoToPeaceModeToUseKits = cell.AsBool(); break;
|
||||||
|
case "userecklessness": c.UseRecklessness = cell.AsBool(); break;
|
||||||
|
case "debuffprecastseconds": c.DebuffPrecastSeconds = cell.AsDouble(); break;
|
||||||
|
case "clearlevelboostflagoncast": v.ClearLevelBoostFlagOnCast = cell.AsBool(); break;
|
||||||
|
case "idlecraftcount_healthkits": i.IdleHealthKitCount = cell.AsInt(); break;
|
||||||
|
case "idlecraftcount_stamkits": i.IdleStaminaKitCount = cell.AsInt(); break;
|
||||||
|
case "idlecraftcount_manakits": i.IdleManaKitCount = cell.AsInt(); break;
|
||||||
|
case "idlecraftcount_healthfood": i.IdleHealthFoodCount = cell.AsInt(); break;
|
||||||
|
case "idlecraftcount_stamfood": i.IdleStaminaFoodCount = cell.AsInt(); break;
|
||||||
|
case "idlecraftcount_manafood": i.IdleManaFoodCount = cell.AsInt(); break;
|
||||||
|
case "buffcastrecast_seconds": b.BuffCastRecastSeconds = cell.AsDouble(); break;
|
||||||
|
case "buffcastrecastreset_seconds": b.BuffCastRecastResetSeconds = cell.AsDouble(); break;
|
||||||
|
case "enablemeta": break; // live MetaEngine.Enabled, not a stored settings field.
|
||||||
|
case "blacklistedspellcomps":
|
||||||
|
b.BlacklistedSpellComponents = cell.AsString();
|
||||||
|
c.BlacklistedSpellComponents = cell.AsString();
|
||||||
|
break;
|
||||||
|
case "droptopeacemoderetrycount": v.DropToPeaceModeRetryCount = cell.AsInt(); break;
|
||||||
|
case "followaroundcorners": n.FollowAroundCorners = cell.AsBool(); break;
|
||||||
|
case "blacklistcorpseopenattemptcount": i.Loot.BlacklistCorpseOpenAttemptCount = cell.AsInt(); break;
|
||||||
|
case "blacklistcorpseopentimeoutseconds": i.Loot.BlacklistCorpseOpenTimeoutSeconds = cell.AsDouble(); break;
|
||||||
|
case "summonpets": c.SummonPets = cell.AsBool(); break;
|
||||||
|
case "petrangemode": c.PetRangeMode = (PetRangeMode)cell.AsInt(); break;
|
||||||
|
case "petcustomrange": c.PetCustomRange = cell.AsDouble() * 240d; break;
|
||||||
|
case "petrefillcount-idle": c.PetRefillCountIdle = cell.AsInt(); break;
|
||||||
|
case "petrefillcount-normal": c.PetRefillCountNormal = cell.AsInt(); break;
|
||||||
|
case "corpseopentimeoutseconds": i.Loot.CorpseOpenTimeoutSeconds = cell.AsDouble(); break;
|
||||||
|
case "petmonsterdensity": c.PetMonsterDensity = cell.AsInt(); break;
|
||||||
|
case "corpselootitemmaxattempts": i.Loot.CorpseLootItemMaxAttempts = cell.AsInt(); break;
|
||||||
|
case "fastcastbuffs": b.FastCastBuffs = cell.AsBool(); break;
|
||||||
|
case "usebreakableturnto": c.UseBreakableTurnTo = cell.AsBool(); break;
|
||||||
|
case "useprojectileawareness": c.UseProjectileAwareness = cell.AsBool(); break;
|
||||||
|
case "collisionprojectileradius": c.CollisionProjectileRadius = cell.AsDouble(); break;
|
||||||
|
case "collisionstepdistance": c.CollisionStepDistance = cell.AsDouble(); break;
|
||||||
|
case "showcollisiondebug": c.ShowCollisionDebug = cell.AsBool(); break;
|
||||||
|
case "maximumcollisioncheckspertick": c.MaximumCollisionChecksPerTick = cell.AsInt(); break;
|
||||||
|
case "spellrangefudge": c.SpellRangeFudge = cell.AsDouble(); break;
|
||||||
|
case "buffwithuntrained-item": b.BuffWithUntrainedItemSkill = cell.AsInt(); break;
|
||||||
|
case "buffwithuntrained-creature": b.BuffWithUntrainedCreatureSkill = cell.AsInt(); break;
|
||||||
|
case "buffwithuntrained-life": b.BuffWithUntrainedLifeSkill = cell.AsInt(); break;
|
||||||
|
case "allowdebufffallback": c.AllowDebuffFallback = cell.AsBool(); break;
|
||||||
|
case "rechargehandlerset":
|
||||||
|
if (cell.Tag == "TABLE" && cell.Table is { } table)
|
||||||
|
v.RechargeHandlerRows = ParseRechargeHandlerSet(table);
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Capture: live setting -> .usd cell. Mirror of Apply above; a name
|
||||||
|
// Apply ignores (enablemeta, rechargehandlerset) also returns null here
|
||||||
|
// so Save leaves that row's original cell untouched.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>Internal (not private) solely so the 137-setting coverage test can call it directly.</summary>
|
||||||
|
internal static VtankCell? Capture(string rawName, AllSettings s)
|
||||||
|
{
|
||||||
|
if (!VtankOptionCatalog.IsKnown(rawName))
|
||||||
|
return null;
|
||||||
|
string name = VtankOptionCatalog.Canonical(rawName).ToLowerInvariant();
|
||||||
|
CombatSettings c = s.Combat;
|
||||||
|
BuffSettings b = s.Buffs;
|
||||||
|
VitalSettings v = s.Vitals;
|
||||||
|
InventorySettings i = s.Inventory;
|
||||||
|
NavigationSettings n = s.Navigation;
|
||||||
|
return name switch
|
||||||
|
{
|
||||||
|
"enablelooting" => VtankCell.Bool(i.Loot.Enabled),
|
||||||
|
"enablenav" => VtankCell.Bool(n.Enabled),
|
||||||
|
"enablebuffing" => VtankCell.Bool(b.Enabled),
|
||||||
|
"enablecombat" => VtankCell.Bool(c.Enabled),
|
||||||
|
"spelldiffexcessthreshold-hunt" => Num(name, c.HuntSkillExcessOverDifficulty),
|
||||||
|
"spelldiffexcessthreshold-buff" => Num(name, b.SkillExcessOverDifficulty),
|
||||||
|
"arrowheadfletchdiffexcessthreshold" => Num(name, i.ArrowheadFletchDifficultyExcess),
|
||||||
|
"recharge-norm-hitp" => Num(name, v.NormalHealth * 100d),
|
||||||
|
"recharge-norm-stam" => Num(name, v.NormalStamina * 100d),
|
||||||
|
"recharge-norm-mana" => Num(name, v.NormalMana * 100d),
|
||||||
|
"recharge-notarg-hitp" => Num(name, v.NoTargetHealth * 100d),
|
||||||
|
"recharge-notarg-stam" => Num(name, v.NoTargetStamina * 100d),
|
||||||
|
"recharge-notarg-mana" => Num(name, v.NoTargetMana * 100d),
|
||||||
|
"recharge-helper-hitp" => Num(name, v.HelperHealth * 100d),
|
||||||
|
"recharge-helper-stam" => Num(name, v.HelperStamina * 100d),
|
||||||
|
"recharge-helper-mana" => Num(name, v.HelperMana * 100d),
|
||||||
|
"dohelp" => VtankCell.Bool(v.HelpOthers),
|
||||||
|
"attackdistance" => Num(name, c.MaximumRange / 240d),
|
||||||
|
"attackminimumdistance" => Num(name, c.MinimumRange / 240d),
|
||||||
|
"approachdistance" => Num(name, c.ApproachDistance / 240d),
|
||||||
|
"ringdistance" => Num(name, c.RingDistance / 240d),
|
||||||
|
"corpseapproachrange-max" => Num(name, i.Loot.CorpseApproachRange / 240d),
|
||||||
|
"corpseapproachrange-min" => Num(name, i.Loot.CorpseMinimumApproachRange / 240d),
|
||||||
|
"navclosestoprange" => Num(name, n.MinimumDistanceMeters / 240d),
|
||||||
|
"navfarstoprange" => Num(name, n.MaximumDistanceMeters / 240d),
|
||||||
|
"useportaldistance" => Num(name, n.PortalUseDistanceMeters / 240d),
|
||||||
|
"helperdistancehitp" => Num(name, v.HelperHealthDistance / 240d),
|
||||||
|
"helperdistancestam" => Num(name, v.HelperStaminaDistance / 240d),
|
||||||
|
"helperdistancemana" => Num(name, v.HelperManaDistance / 240d),
|
||||||
|
"minimumringtargets" => Num(name, c.MinimumRingTargets),
|
||||||
|
"defaultmeleeattackheight" => Num(name, (int)c.AttackHeight),
|
||||||
|
"castdispelself" => VtankCell.Bool(v.CastDispelSelf),
|
||||||
|
"usedispelitems" => VtankCell.Bool(v.UseDispelItems),
|
||||||
|
"autocram" => VtankCell.Bool(i.AutoCram),
|
||||||
|
"autostack" => VtankCell.Bool(i.AutoStack),
|
||||||
|
"readunknownscrolls" => VtankCell.Bool(i.Loot.ReadUnknownScrolls),
|
||||||
|
"usedispeldrum" => VtankCell.Bool(v.UseDispelDrum),
|
||||||
|
"switchwandstodebuff" => VtankCell.Bool(c.SwitchWandsToDebuff),
|
||||||
|
"autocraftitems" => VtankCell.Bool(i.AutoCraftItems),
|
||||||
|
"usehealersheart" => VtankCell.Bool(v.UseHealersHeart),
|
||||||
|
"jumpoutwandcasting" => VtankCell.Bool(c.JumpOutWandCasting),
|
||||||
|
"lootallcorpses" => VtankCell.Bool(i.Loot.LootAllCorpses),
|
||||||
|
"lootfellowcorpses" => VtankCell.Bool(i.Loot.LootFellowCorpses),
|
||||||
|
"dojiggle" => VtankCell.Bool(c.DoJiggle),
|
||||||
|
"randomhelperbuffs" => VtankCell.Bool(b.RandomHelperBuffs),
|
||||||
|
"randomhelperintervalseconds" => Num(name, b.RandomHelperIntervalSeconds),
|
||||||
|
"idlepeacemode" => VtankCell.Bool(c.IdlePeaceMode),
|
||||||
|
"targetlock" => VtankCell.Bool(c.TargetLock),
|
||||||
|
"stopmacroondeath" => VtankCell.Bool(c.StopMacroOnDeath),
|
||||||
|
"usearcs" => Num(name, (int)c.UseArcs),
|
||||||
|
"arcrange" => Num(name, c.ArcRange / 240d),
|
||||||
|
"targetselectmethod" => Num(name, (int)c.SelectionMethod + 1),
|
||||||
|
"targetselectanglerange" => Num(name, c.TargetSelectAngleRange / 240d),
|
||||||
|
"idlebufftopoff" => VtankCell.Bool(b.IdleBuffTopoff),
|
||||||
|
"idlebufftopofftimeseconds" => Num(name, b.IdleBuffTopoffSeconds),
|
||||||
|
"rebufftimeremainingseconds" => Num(name, b.RebuffWhenUnderSeconds),
|
||||||
|
"refillwornmana" => VtankCell.Bool(i.RefillWornMana),
|
||||||
|
"refillwornmana-item-manapercent" => Num(name, i.RefillWornManaPercent),
|
||||||
|
"buffprofile-prots" => VtankCell.String(b.ProtectionElements),
|
||||||
|
"buffprofile-banes" => VtankCell.String(b.BaneElements),
|
||||||
|
"buffprofile_prots" => Num(name, b.ProtectionProfileMode),
|
||||||
|
"buffprofile_banes" => Num(name, b.BaneProfileMode),
|
||||||
|
"debuffeachfirst" => Num(name, (int)c.DebuffEachFirst),
|
||||||
|
"autoattackpower" => VtankCell.Bool(c.AutoAttackPower),
|
||||||
|
"lootpriorityboost" => VtankCell.Bool(i.Loot.PriorityBoost),
|
||||||
|
"corpsecachetimeoutminutes" => Num(name, i.Loot.CorpseCacheTimeoutMinutes),
|
||||||
|
"corpseitemappearancetimeoutseconds" => Num(name, i.Loot.CorpseItemAppearanceTimeoutSeconds),
|
||||||
|
"corpseitemidtimeoutseconds" => Num(name, i.Loot.CorpseItemIdentifyTimeoutSeconds),
|
||||||
|
"debuffselectionmethod" => Num(name, (int)c.DebuffSelectionMethod),
|
||||||
|
"manastonelootcount" => Num(name, i.Loot.ManaStoneLootCount),
|
||||||
|
"manatankminimummana" => Num(name, i.Loot.ManaTankMinimumMana),
|
||||||
|
"splitpeas" => VtankCell.Bool(i.SplitPeas),
|
||||||
|
"spellcompmin-critical" => Num(name, i.CriticalComponentMinimum),
|
||||||
|
"spellcompmin-normal" => Num(name, i.NormalComponentMinimum),
|
||||||
|
"spellcompmin-idle" => Num(name, i.IdleComponentMinimum),
|
||||||
|
"rechargeboosttimeseconds" => Num(name, v.RechargeBoostTimeSeconds),
|
||||||
|
"rechargeboostamount" => Num(name, v.RechargeBoostAmount),
|
||||||
|
"usespecialammo" => Num(name, c.UseSpecialAmmo),
|
||||||
|
"opendoors" => VtankCell.Bool(n.OpenDoors),
|
||||||
|
"dooridrange" => Num(name, n.DoorIdentifyRangeMeters / 240d),
|
||||||
|
"dooropenrange" => Num(name, n.DoorOpenRangeMeters / 240d),
|
||||||
|
"doorlockpickdiffexcessthreshold" => Num(name, n.DoorLockpickExcessThreshold),
|
||||||
|
"manachargeswhenoff" => VtankCell.Bool(i.ManaChargesWhenOff),
|
||||||
|
"autofellowmanagement" => VtankCell.Bool(c.AutoFellowManagement),
|
||||||
|
"minimumhealkitsuccesschance" => Num(name, v.MinimumHealKitSuccessChance),
|
||||||
|
"usekitsinmagicmode" => VtankCell.Bool(v.UseKitsInMagicMode),
|
||||||
|
"staminatohealthmultiplier" => Num(name, v.StaminaToHealthMultiplier),
|
||||||
|
"manatohealthmultiplier" => Num(name, v.ManaToHealthMultiplier),
|
||||||
|
"navpriorityboost" => VtankCell.Bool(n.Priority),
|
||||||
|
"deleteghostmonsters" => VtankCell.Bool(c.DeleteGhostMonsters),
|
||||||
|
"ghostmonsterspellattemptcount" => Num(name, c.GhostMonsterSpellAttemptCount),
|
||||||
|
"whoyougonnacall" => VtankCell.Bool(c.WhoYouGonnaCall),
|
||||||
|
"blacklistmonsterattemptcount" => Num(name, c.BlacklistMonsterAttemptCount),
|
||||||
|
"blacklistmonstertimeoutseconds" => Num(name, c.BlacklistMonsterTimeoutSeconds),
|
||||||
|
"combinesalvage" => VtankCell.Bool(i.Loot.CombineSalvage),
|
||||||
|
"lootonlyrarecorpses" => VtankCell.Bool(i.Loot.LootOnlyRareCorpses),
|
||||||
|
"deleteghostmonstersbyhptracker" => VtankCell.Bool(c.DeleteGhostMonstersByHealthTracker),
|
||||||
|
"ghostdeletehptrackerseconds" => Num(name, c.GhostDeleteHealthTrackerSeconds),
|
||||||
|
"gotopeacemodetousekits" => VtankCell.Bool(v.GoToPeaceModeToUseKits),
|
||||||
|
"userecklessness" => VtankCell.Bool(c.UseRecklessness),
|
||||||
|
"debuffprecastseconds" => Num(name, c.DebuffPrecastSeconds),
|
||||||
|
"clearlevelboostflagoncast" => VtankCell.Bool(v.ClearLevelBoostFlagOnCast),
|
||||||
|
"idlecraftcount_healthkits" => Num(name, i.IdleHealthKitCount),
|
||||||
|
"idlecraftcount_stamkits" => Num(name, i.IdleStaminaKitCount),
|
||||||
|
"idlecraftcount_manakits" => Num(name, i.IdleManaKitCount),
|
||||||
|
"idlecraftcount_healthfood" => Num(name, i.IdleHealthFoodCount),
|
||||||
|
"idlecraftcount_stamfood" => Num(name, i.IdleStaminaFoodCount),
|
||||||
|
"idlecraftcount_manafood" => Num(name, i.IdleManaFoodCount),
|
||||||
|
"buffcastrecast_seconds" => Num(name, b.BuffCastRecastSeconds),
|
||||||
|
"buffcastrecastreset_seconds" => Num(name, b.BuffCastRecastResetSeconds),
|
||||||
|
"blacklistedspellcomps" => VtankCell.String(b.BlacklistedSpellComponents),
|
||||||
|
"droptopeacemoderetrycount" => Num(name, v.DropToPeaceModeRetryCount),
|
||||||
|
"followaroundcorners" => VtankCell.Bool(n.FollowAroundCorners),
|
||||||
|
"blacklistcorpseopenattemptcount" => Num(name, i.Loot.BlacklistCorpseOpenAttemptCount),
|
||||||
|
"blacklistcorpseopentimeoutseconds" => Num(name, i.Loot.BlacklistCorpseOpenTimeoutSeconds),
|
||||||
|
"summonpets" => VtankCell.Bool(c.SummonPets),
|
||||||
|
"petrangemode" => Num(name, (int)c.PetRangeMode),
|
||||||
|
"petcustomrange" => Num(name, c.PetCustomRange / 240d),
|
||||||
|
"petrefillcount-idle" => Num(name, c.PetRefillCountIdle),
|
||||||
|
"petrefillcount-normal" => Num(name, c.PetRefillCountNormal),
|
||||||
|
"corpseopentimeoutseconds" => Num(name, i.Loot.CorpseOpenTimeoutSeconds),
|
||||||
|
"petmonsterdensity" => Num(name, c.PetMonsterDensity),
|
||||||
|
"corpselootitemmaxattempts" => Num(name, i.Loot.CorpseLootItemMaxAttempts),
|
||||||
|
"fastcastbuffs" => VtankCell.Bool(b.FastCastBuffs),
|
||||||
|
"usebreakableturnto" => VtankCell.Bool(c.UseBreakableTurnTo),
|
||||||
|
"useprojectileawareness" => VtankCell.Bool(c.UseProjectileAwareness),
|
||||||
|
"collisionprojectileradius" => Num(name, c.CollisionProjectileRadius),
|
||||||
|
"collisionstepdistance" => Num(name, c.CollisionStepDistance),
|
||||||
|
"showcollisiondebug" => VtankCell.Bool(c.ShowCollisionDebug),
|
||||||
|
"maximumcollisioncheckspertick" => Num(name, c.MaximumCollisionChecksPerTick),
|
||||||
|
"spellrangefudge" => Num(name, c.SpellRangeFudge),
|
||||||
|
"buffwithuntrained-item" => Num(name, b.BuffWithUntrainedItemSkill),
|
||||||
|
"buffwithuntrained-creature" => Num(name, b.BuffWithUntrainedCreatureSkill),
|
||||||
|
"buffwithuntrained-life" => Num(name, b.BuffWithUntrainedLifeSkill),
|
||||||
|
"allowdebufffallback" => VtankCell.Bool(c.AllowDebuffFallback),
|
||||||
|
_ => null, // "enablemeta" (live engine state) and "rechargehandlerset"
|
||||||
|
// (no write path exists in real VTank either, section 2 row 137)
|
||||||
|
// are deliberately left untouched.
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses the RechargeHandlerSet nested TABLE (5 columns:
|
||||||
|
/// Vital/HandlerString/MinPercent/MaxPercent/Stance — verified against
|
||||||
|
/// the live <c>defaultsettings.usd</c> fixture, 26 rows).
|
||||||
|
/// </summary>
|
||||||
|
internal static RechargeHandlerRow[] ParseRechargeHandlerSet(VtankTable table)
|
||||||
|
{
|
||||||
|
int vitalColumn = table.ColumnIndex("Vital");
|
||||||
|
int handlerColumn = table.ColumnIndex("HandlerString");
|
||||||
|
int minColumn = table.ColumnIndex("MinPercent");
|
||||||
|
int maxColumn = table.ColumnIndex("MaxPercent");
|
||||||
|
int stanceColumn = table.ColumnIndex("Stance");
|
||||||
|
if (vitalColumn < 0 || handlerColumn < 0 || minColumn < 0
|
||||||
|
|| maxColumn < 0 || stanceColumn < 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
var rows = new RechargeHandlerRow[table.Rows.Count];
|
||||||
|
for (int i = 0; i < table.Rows.Count; i++)
|
||||||
|
{
|
||||||
|
VtankRow row = table.Rows[i];
|
||||||
|
rows[i] = new RechargeHandlerRow(
|
||||||
|
row.Cells[vitalColumn].AsInt(),
|
||||||
|
row.Cells[handlerColumn].AsString(),
|
||||||
|
row.Cells[minColumn].AsInt(),
|
||||||
|
row.Cells[maxColumn].AsInt(),
|
||||||
|
row.Cells[stanceColumn].AsInt());
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No RenderRechargeHandlerSet/write-back helper exists here deliberately:
|
||||||
|
// real VTank has NO save path for this table at all (docs/research/
|
||||||
|
// vtank-kb/01-settings-and-profiles.md section 2, row 137 — tCustom has
|
||||||
|
// no case in the Advanced Options editor's display/edit switch or in
|
||||||
|
// "/vt opt set"; the table is owned by a dedicated cRechargeManager
|
||||||
|
// object, not any control that persists through the generic Settings
|
||||||
|
// save path). Writing it back would be a MossTank invention, not a
|
||||||
|
// retail port. A prior version of this file had an unused
|
||||||
|
// RenderRechargeHandlerSet helper for exactly that invention; it was
|
||||||
|
// deleted rather than wired in for this reason.
|
||||||
|
}
|
||||||
438
src/AcDream.Plugins.MossTank/VtankUsdDocument.cs
Normal file
438
src/AcDream.Plugins.MossTank/VtankUsdDocument.cs
Normal file
|
|
@ -0,0 +1,438 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace AcDream.Plugins.MossTank;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The self-describing recursive text-database grammar VTank uses for
|
||||||
|
/// <c>.usd</c> settings profiles and <c>.ast</c> per-character caches. Ported
|
||||||
|
/// from the decompiled <c>gy</c>/<c>cw</c>/<c>bd</c>/<c>y</c> classes
|
||||||
|
/// documented in <c>docs/research/vtank-kb/01-settings-and-profiles.md</c>
|
||||||
|
/// section 1: <c>refs/vtank/decompiled/gy.cs:20-56</c> (typed cell),
|
||||||
|
/// <c>cw.cs:9-40</c> (row), <c>bd.cs:364-395</c> (table), <c>y.cs:87-104</c>
|
||||||
|
/// (whole database). Plain text, CRLF line endings, no compression, no
|
||||||
|
/// checksum, no top-level version header.
|
||||||
|
///
|
||||||
|
/// One cell (<c>gy</c>) is a type-tag line followed by zero or more value
|
||||||
|
/// lines: <c>d</c>=double, <c>i</c>=int, <c>u</c>=uint, <c>f</c>=float,
|
||||||
|
/// <c>s</c>=string (the raw next line, possibly empty), <c>b</c>=bool
|
||||||
|
/// (<c>"True"</c>/<c>"False"</c>), <c>TABLE</c>=a nested table (recurse into
|
||||||
|
/// <see cref="VtankTable"/>), <c>ba</c>=a length-prefixed raw-character blob
|
||||||
|
/// (an int line for the character count, then that many raw characters,
|
||||||
|
/// not lines, so embedded newlines survive — see <see cref="VtankLineCursor.ReadBlob"/>).
|
||||||
|
/// Any other tag (<c>y.cs:28-41</c> only ever registers <c>TABLE</c> and
|
||||||
|
/// <c>ba</c> as named custom types) is genuinely unrecognized to VTank
|
||||||
|
/// itself: <c>gy.cs:50-55</c> (read) consumes ONLY the tag line and
|
||||||
|
/// <c>gy.cs:98-101</c> (write) emits ONLY a tag line ("0" for a void cell)
|
||||||
|
/// — no value line either way. This is preserved verbatim so an
|
||||||
|
/// unrecognized custom type still round-trips byte-for-byte.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class VtankCell
|
||||||
|
{
|
||||||
|
// The only tags VTank's gy.a(TextReader)/gy.a(TextWriter) treat as a
|
||||||
|
// tag+value pair (gy.cs:20-56, gy.cs:60-92). TABLE and ba are each
|
||||||
|
// handled by their own dedicated branch; everything else is an
|
||||||
|
// unrecognized/void tag that carries no value line at all.
|
||||||
|
private static readonly HashSet<string> KnownScalarTags = ["d", "i", "u", "f", "s", "b"];
|
||||||
|
|
||||||
|
internal VtankCell() { }
|
||||||
|
|
||||||
|
/// <summary>The raw type tag: <c>d</c>/<c>i</c>/<c>u</c>/<c>f</c>/<c>s</c>/<c>b</c>/<c>TABLE</c>/<c>ba</c>/other.</summary>
|
||||||
|
public required string Tag { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Raw text of the single value line, for every scalar tag except <c>ba</c>.</summary>
|
||||||
|
public string? ScalarText { get; set; }
|
||||||
|
|
||||||
|
/// <summary>The nested table payload, only when <see cref="Tag"/> is <c>TABLE</c>.</summary>
|
||||||
|
public VtankTable? Table { get; set; }
|
||||||
|
|
||||||
|
/// <summary>The raw character blob, only when <see cref="Tag"/> is <c>ba</c>.</summary>
|
||||||
|
public string? BlobText { get; set; }
|
||||||
|
|
||||||
|
public static VtankCell Double(double value) => new()
|
||||||
|
{
|
||||||
|
Tag = "d",
|
||||||
|
ScalarText = FormatDouble(value),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static VtankCell Int(int value) => new()
|
||||||
|
{
|
||||||
|
Tag = "i",
|
||||||
|
ScalarText = value.ToString(CultureInfo.InvariantCulture),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static VtankCell UInt(uint value) => new()
|
||||||
|
{
|
||||||
|
Tag = "u",
|
||||||
|
ScalarText = value.ToString(CultureInfo.InvariantCulture),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static VtankCell Float(float value) => new()
|
||||||
|
{
|
||||||
|
Tag = "f",
|
||||||
|
ScalarText = FormatDouble(value),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static VtankCell String(string value) => new()
|
||||||
|
{
|
||||||
|
Tag = "s",
|
||||||
|
// VTank strips embedded newlines from a string cell on write
|
||||||
|
// (gy.cs:84: `text.Replace("\n", "")`) — a string cell is always
|
||||||
|
// exactly one line; a blob that needs embedded newlines uses "ba".
|
||||||
|
ScalarText = value.Replace("\n", string.Empty, StringComparison.Ordinal),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static VtankCell Bool(bool value) => new()
|
||||||
|
{
|
||||||
|
Tag = "b",
|
||||||
|
ScalarText = value ? "True" : "False",
|
||||||
|
};
|
||||||
|
|
||||||
|
public static VtankCell NestedTable(VtankTable table) => new()
|
||||||
|
{
|
||||||
|
Tag = "TABLE",
|
||||||
|
Table = table,
|
||||||
|
};
|
||||||
|
|
||||||
|
public double AsDouble() => double.Parse(
|
||||||
|
ScalarText ?? "0",
|
||||||
|
NumberStyles.Float,
|
||||||
|
CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
public int AsInt() => int.Parse(
|
||||||
|
ScalarText ?? "0",
|
||||||
|
NumberStyles.Integer,
|
||||||
|
CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
public uint AsUInt() => uint.Parse(
|
||||||
|
ScalarText ?? "0",
|
||||||
|
NumberStyles.Integer,
|
||||||
|
CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
public float AsFloat() => float.Parse(
|
||||||
|
ScalarText ?? "0",
|
||||||
|
NumberStyles.Float,
|
||||||
|
CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
public string AsString() => ScalarText ?? string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VTank's <c>gy</c> bool tag stores the literal text <c>"True"</c> or
|
||||||
|
/// <c>"False"</c> (C#'s default <see cref="bool"/> formatting).
|
||||||
|
/// </summary>
|
||||||
|
public bool AsBool() => string.Equals(ScalarText, "True", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formats a double/float the way VTank's own (older .NET Framework)
|
||||||
|
/// build does: <c>Convert.ToString(double, CultureInfo)</c>
|
||||||
|
/// (<c>gy.cs:60-61</c>) used the CLR's classic 15-significant-digit
|
||||||
|
/// general format, not .NET Core's shortest-round-trippable default.
|
||||||
|
/// Verified against every <c>d</c>-tagged value in the real
|
||||||
|
/// <c>defaultsettings.usd</c> fixture (e.g. 5/240 renders as
|
||||||
|
/// <c>0.0208333333333333</c> — 15 significant digits).
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatDouble(double value) =>
|
||||||
|
value.ToString("G15", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
internal void WriteTo(StringBuilder sb)
|
||||||
|
{
|
||||||
|
VtankWriter.AppendLine(sb, Tag);
|
||||||
|
if (Tag == "TABLE")
|
||||||
|
{
|
||||||
|
Table!.WriteTo(sb);
|
||||||
|
}
|
||||||
|
else if (Tag == "ba")
|
||||||
|
{
|
||||||
|
// A "ba" blob is a raw character count, not a line count: the
|
||||||
|
// blob may contain embedded newlines. VTank's own writer
|
||||||
|
// (f6.cs:20-24) WriteLine()s only the length, then Write()s the
|
||||||
|
// raw characters with NO trailing line terminator — whatever
|
||||||
|
// structural token follows continues immediately after the
|
||||||
|
// blob's last character, exactly like the real client's output.
|
||||||
|
string blob = BlobText ?? string.Empty;
|
||||||
|
VtankWriter.AppendLine(sb, blob.Length.ToString(CultureInfo.InvariantCulture));
|
||||||
|
sb.Append(blob);
|
||||||
|
}
|
||||||
|
else if (KnownScalarTags.Contains(Tag))
|
||||||
|
{
|
||||||
|
VtankWriter.AppendLine(sb, ScalarText ?? string.Empty);
|
||||||
|
}
|
||||||
|
// else: an unrecognized/void tag is the WHOLE cell (gy.cs:98-101
|
||||||
|
// writes only "0", no value line) — nothing further to emit.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Shared line-terminator helper for every <c>WriteTo</c> in this file (CRLF, matching VTank's own writer).</summary>
|
||||||
|
internal static class VtankWriter
|
||||||
|
{
|
||||||
|
internal static void AppendLine(StringBuilder sb, string text) =>
|
||||||
|
sb.Append(text).Append("\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class VtankRow
|
||||||
|
{
|
||||||
|
public List<VtankCell> Cells { get; } = [];
|
||||||
|
|
||||||
|
internal void WriteTo(StringBuilder sb)
|
||||||
|
{
|
||||||
|
foreach (VtankCell cell in Cells)
|
||||||
|
cell.WriteTo(sb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One <c>bd</c> table: a column-name list, a hash-index flag per column,
|
||||||
|
/// and a row list. Row length is NOT fixed at parse time — a <c>TABLE</c> or
|
||||||
|
/// <c>ba</c> cell recurses, so the reader must walk cell-by-cell rather than
|
||||||
|
/// assume a fixed line stride per row (<c>bd.cs:364-395</c>).
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class VtankTable
|
||||||
|
{
|
||||||
|
public List<string> ColumnNames { get; } = [];
|
||||||
|
public List<bool> IndexFlags { get; } = [];
|
||||||
|
public List<VtankRow> Rows { get; } = [];
|
||||||
|
|
||||||
|
public int ColumnIndex(string name) => ColumnNames.FindIndex(
|
||||||
|
column => column.Equals(name, StringComparison.Ordinal));
|
||||||
|
|
||||||
|
internal static VtankTable Read(VtankLineCursor cursor)
|
||||||
|
{
|
||||||
|
var table = new VtankTable();
|
||||||
|
int columnCount = cursor.ReadInt();
|
||||||
|
for (int i = 0; i < columnCount; i++)
|
||||||
|
table.ColumnNames.Add(cursor.ReadLine());
|
||||||
|
for (int i = 0; i < columnCount; i++)
|
||||||
|
table.IndexFlags.Add(cursor.ReadLine() == "y");
|
||||||
|
int rowCount = cursor.ReadInt();
|
||||||
|
for (int r = 0; r < rowCount; r++)
|
||||||
|
{
|
||||||
|
var row = new VtankRow();
|
||||||
|
for (int c = 0; c < columnCount; c++)
|
||||||
|
row.Cells.Add(VtankDatabaseReader.ReadCell(cursor));
|
||||||
|
table.Rows.Add(row);
|
||||||
|
}
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void WriteTo(StringBuilder sb)
|
||||||
|
{
|
||||||
|
VtankWriter.AppendLine(sb, ColumnNames.Count.ToString(CultureInfo.InvariantCulture));
|
||||||
|
foreach (string column in ColumnNames)
|
||||||
|
VtankWriter.AppendLine(sb, column);
|
||||||
|
foreach (bool flag in IndexFlags)
|
||||||
|
VtankWriter.AppendLine(sb, flag ? "y" : "n");
|
||||||
|
VtankWriter.AppendLine(sb, Rows.Count.ToString(CultureInfo.InvariantCulture));
|
||||||
|
foreach (VtankRow row in Rows)
|
||||||
|
row.WriteTo(sb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The whole <c>y</c> database: an ordered set of named tables.</summary>
|
||||||
|
internal sealed class VtankDatabase
|
||||||
|
{
|
||||||
|
public List<(string Name, VtankTable Table)> Tables { get; } = [];
|
||||||
|
|
||||||
|
public VtankTable? Find(string name) => Tables
|
||||||
|
.Where(entry => entry.Name.Equals(name, StringComparison.Ordinal))
|
||||||
|
.Select(static entry => entry.Table)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
public static VtankDatabase Parse(string text)
|
||||||
|
{
|
||||||
|
var cursor = new VtankLineCursor(text);
|
||||||
|
var database = new VtankDatabase();
|
||||||
|
int tableCount = cursor.ReadInt();
|
||||||
|
for (int i = 0; i < tableCount; i++)
|
||||||
|
{
|
||||||
|
string name = cursor.ReadLine();
|
||||||
|
VtankTable table = VtankTable.Read(cursor);
|
||||||
|
database.Tables.Add((name, table));
|
||||||
|
}
|
||||||
|
return database;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VTank's own <c>y</c> class holds its tables in a
|
||||||
|
/// <c>SortedDictionary<string, bd></c> (item J, slice-1 fix
|
||||||
|
/// round), so a real <c>.usd</c>/<c>.ast</c> always emits its tables in
|
||||||
|
/// table-name order — confirmed against the committed
|
||||||
|
/// <c>defaultsettings.usd</c> fixture, whose first several tables
|
||||||
|
/// (AntiExtraBuffSpells, AssistItems, BuffedItems, ExtraBuffSpells,
|
||||||
|
/// GemFoodItems, …) are already alphabetical. This port's own
|
||||||
|
/// <see cref="Tables"/> is an insertion-ordered list rather than a
|
||||||
|
/// sorted structure, so every committed real fixture happens to
|
||||||
|
/// round-trip in order today purely because it was ALREADY sorted the
|
||||||
|
/// last time real VTank wrote it — this explicit sort on render makes
|
||||||
|
/// that invariant a property of the writer itself rather than an
|
||||||
|
/// accident of whatever order the source data happened to arrive in
|
||||||
|
/// (no observable behavior change for any fixture today; it only
|
||||||
|
/// matters the day a future code path adds or reorders a table).
|
||||||
|
///
|
||||||
|
/// Round 3 item 12: <see cref="StringComparer.Ordinal"/> is used here
|
||||||
|
/// as a stand-in for .NET Framework's <c>SortedDictionary<string, bd></c>
|
||||||
|
/// default key order — that equivalence is confirmed only for the
|
||||||
|
/// plain-ASCII, single-case table names VTank itself ships
|
||||||
|
/// (AntiExtraBuffSpells, MyMonsters, Settings, …), not as a general
|
||||||
|
/// claim that ordinal and .NET's culture-aware default string
|
||||||
|
/// comparer agree for every possible string; a hypothetical future
|
||||||
|
/// table name using non-ASCII characters or mixed-width comparisons
|
||||||
|
/// could sort differently under the two. No behavior change is
|
||||||
|
/// implied or made by this note.
|
||||||
|
/// </summary>
|
||||||
|
public string Render()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var ordered = Tables
|
||||||
|
.OrderBy(static entry => entry.Name, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
VtankWriter.AppendLine(sb, ordered.Length.ToString(CultureInfo.InvariantCulture));
|
||||||
|
foreach ((string name, VtankTable table) in ordered)
|
||||||
|
{
|
||||||
|
VtankWriter.AppendLine(sb, name);
|
||||||
|
table.WriteTo(sb);
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Reads one <see cref="VtankCell"/>, recursing into nested tables and blobs.</summary>
|
||||||
|
internal static class VtankDatabaseReader
|
||||||
|
{
|
||||||
|
public static VtankCell ReadCell(VtankLineCursor cursor)
|
||||||
|
{
|
||||||
|
string tag = cursor.ReadLine();
|
||||||
|
switch (tag)
|
||||||
|
{
|
||||||
|
case "TABLE":
|
||||||
|
return VtankCell.NestedTable(VtankTable.Read(cursor));
|
||||||
|
case "ba":
|
||||||
|
int length = cursor.ReadInt();
|
||||||
|
return new VtankCellBuilder(tag) { BlobText = cursor.ReadBlob(length) }
|
||||||
|
.Build();
|
||||||
|
case "d" or "i" or "u" or "f" or "s" or "b":
|
||||||
|
return new VtankCellBuilder(tag) { ScalarText = cursor.ReadLine() }.Build();
|
||||||
|
default:
|
||||||
|
// Unrecognized/void tag (refs/vtank/decompiled/y.cs:28-41
|
||||||
|
// only ever registers TABLE and ba as named custom types):
|
||||||
|
// VTank's own reader (gy.cs:50-55) consumes ONLY the tag
|
||||||
|
// line here — no value line follows.
|
||||||
|
return new VtankCellBuilder(tag).Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small builder so VtankCell's constructor can stay private to the file
|
||||||
|
// (its public factory methods are the intended typed-construction path)
|
||||||
|
// while the reader still needs to synthesize an arbitrary/unknown tag.
|
||||||
|
private readonly struct VtankCellBuilder(string tag)
|
||||||
|
{
|
||||||
|
public string? ScalarText { get; init; }
|
||||||
|
public string? BlobText { get; init; }
|
||||||
|
|
||||||
|
public VtankCell Build() => VtankCellFactory.Create(tag, ScalarText, BlobText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Internal factory seam so the reader can build cells with an arbitrary tag.</summary>
|
||||||
|
internal static class VtankCellFactory
|
||||||
|
{
|
||||||
|
public static VtankCell Create(string tag, string? scalarText, string? blobText) => new()
|
||||||
|
{
|
||||||
|
Tag = tag,
|
||||||
|
ScalarText = scalarText,
|
||||||
|
BlobText = blobText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Character-oriented cursor over the exact (unnormalized) text of a
|
||||||
|
/// <c>.usd</c>/<c>.ast</c> file. VTank's own reader is a strict token stream
|
||||||
|
/// (<c>StreamReader.ReadLine()</c>/<c>TextReader.Read(char[], int, int)</c>
|
||||||
|
/// calls in a fixed order) — there is no way to resynchronize after a parse
|
||||||
|
/// error, so every read here throws immediately with a file:line-shaped
|
||||||
|
/// message on the first unexpected token.
|
||||||
|
///
|
||||||
|
/// The cursor deliberately keeps the ORIGINAL string, never pre-splitting on
|
||||||
|
/// <c>'\n'</c>: a <c>ba</c> blob (<see cref="ReadBlob"/>) reads exactly N raw
|
||||||
|
/// characters straight out of that string, embedded <c>\r</c>/<c>\n</c>
|
||||||
|
/// included, matching <c>f6.cs:10-17</c>'s <c>TextReader.Read(array, 0, num)</c>
|
||||||
|
/// exactly — a CRLF-terminated line INSIDE a blob counts as 2 characters
|
||||||
|
/// toward that length, same as it would for VTank itself.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class VtankLineCursor
|
||||||
|
{
|
||||||
|
private readonly string _text;
|
||||||
|
private int _position;
|
||||||
|
private int _lineNumber = 1;
|
||||||
|
|
||||||
|
public VtankLineCursor(string text)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(text);
|
||||||
|
_text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int LineNumber => _lineNumber;
|
||||||
|
|
||||||
|
public string ReadLine()
|
||||||
|
{
|
||||||
|
if (_position >= _text.Length)
|
||||||
|
{
|
||||||
|
throw new FormatException(
|
||||||
|
$"line {LineNumber}: unexpected end of file (expected another line).");
|
||||||
|
}
|
||||||
|
int newlineIndex = _text.IndexOf('\n', _position);
|
||||||
|
string line;
|
||||||
|
if (newlineIndex < 0)
|
||||||
|
{
|
||||||
|
line = _text[_position..];
|
||||||
|
_position = _text.Length;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
line = _text[_position..newlineIndex];
|
||||||
|
_position = newlineIndex + 1;
|
||||||
|
}
|
||||||
|
if (line.EndsWith('\r'))
|
||||||
|
line = line[..^1];
|
||||||
|
_lineNumber++;
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ReadInt()
|
||||||
|
{
|
||||||
|
string line = ReadLine();
|
||||||
|
if (!int.TryParse(line, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value))
|
||||||
|
{
|
||||||
|
throw new FormatException(
|
||||||
|
$"line {LineNumber - 1}: expected an integer, got '{line}'.");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads a <c>ba</c> blob's exact character count straight out of the
|
||||||
|
/// underlying text with no line-oriented interpretation whatsoever
|
||||||
|
/// (<c>f6.cs:10-17</c>: <c>TextReader.Read(array, 0, num)</c> starting
|
||||||
|
/// immediately after the length line's terminator) — embedded
|
||||||
|
/// <c>\r</c>/<c>\n</c> characters are counted and returned verbatim, not
|
||||||
|
/// folded or re-joined.
|
||||||
|
/// </summary>
|
||||||
|
public string ReadBlob(int length)
|
||||||
|
{
|
||||||
|
if (length <= 0)
|
||||||
|
return string.Empty;
|
||||||
|
if (_position + length > _text.Length)
|
||||||
|
{
|
||||||
|
throw new FormatException(
|
||||||
|
$"line {LineNumber}: blob of {length} characters exceeds remaining input.");
|
||||||
|
}
|
||||||
|
string blob = _text.Substring(_position, length);
|
||||||
|
_position += length;
|
||||||
|
foreach (char c in blob)
|
||||||
|
{
|
||||||
|
if (c == '\n')
|
||||||
|
_lineNumber++;
|
||||||
|
}
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -136,6 +136,9 @@
|
||||||
onclick="{CreateProfile}" />
|
onclick="{CreateProfile}" />
|
||||||
<button x="448" y="36" w="112" h="23" text="Clear profile!"
|
<button x="448" y="36" w="112" h="23" text="Clear profile!"
|
||||||
onclick="{ClearProfile}" />
|
onclick="{ClearProfile}" />
|
||||||
|
<button x="568" y="36" w="76" h="23" text="Delete"
|
||||||
|
onclick="{DeleteProfile}"
|
||||||
|
tooltip="Delete the selected named profile's real .usd file. Not available for By char." />
|
||||||
|
|
||||||
<toggle x="580" y="4" w="120" h="20" text="Mine only"
|
<toggle x="580" y="4" w="120" h="20" text="Mine only"
|
||||||
checked="{MineOnlyEnabled}" onclick="{ToggleMineOnly}" />
|
checked="{MineOnlyEnabled}" onclick="{ToggleMineOnly}" />
|
||||||
|
|
@ -155,6 +158,9 @@
|
||||||
onclick="{CreateRouteProfile}" />
|
onclick="{CreateRouteProfile}" />
|
||||||
<button x="560" y="64" w="84" h="22" text="Clear route"
|
<button x="560" y="64" w="84" h="22" text="Clear route"
|
||||||
onclick="{ClearRouteProfile}" />
|
onclick="{ClearRouteProfile}" />
|
||||||
|
<button x="650" y="64" w="70" h="22" text="Delete"
|
||||||
|
onclick="{DeleteRouteProfile}"
|
||||||
|
tooltip="Delete the selected named route's real .af file. Not available for By char." />
|
||||||
<toggle x="4" y="92" w="150" h="20" text="Enable Looting"
|
<toggle x="4" y="92" w="150" h="20" text="Enable Looting"
|
||||||
checked="{LootEnabled}" onclick="{ToggleLooting}" />
|
checked="{LootEnabled}" onclick="{ToggleLooting}" />
|
||||||
<menu x="156" y="90" w="116" h="22" items="{LootProfileNames}"
|
<menu x="156" y="90" w="116" h="22" items="{LootProfileNames}"
|
||||||
|
|
@ -454,9 +460,12 @@
|
||||||
onclick="{CopyMetaProfile}" />
|
onclick="{CopyMetaProfile}" />
|
||||||
<button x="378" y="0" w="48" h="21" text="Clear"
|
<button x="378" y="0" w="48" h="21" text="Clear"
|
||||||
onclick="{ClearMetaProfile}" />
|
onclick="{ClearMetaProfile}" />
|
||||||
<toggle x="446" y="2" w="112" h="20" text="Enable Meta"
|
<button x="428" y="0" w="66" h="21" text="Delete"
|
||||||
|
onclick="{DeleteMetaProfile}"
|
||||||
|
tooltip="Delete the selected named Meta profile's real .af file. Not available for By char." />
|
||||||
|
<toggle x="500" y="2" w="112" h="20" text="Enable Meta"
|
||||||
checked="{MetaEnabled}" onclick="{ToggleMeta}" />
|
checked="{MetaEnabled}" onclick="{ToggleMeta}" />
|
||||||
<label x="566" y="4" text="{MetaStateText}" color="#FFE8DEC3" />
|
<label x="620" y="4" text="{MetaStateText}" color="#FFE8DEC3" />
|
||||||
|
|
||||||
<list x="4" y="26" w="776" h="68" rowheight="17"
|
<list x="4" y="26" w="776" h="68" rowheight="17"
|
||||||
items="{MetaRows}" selected="{SelectedMetaRuleIndex}"
|
items="{MetaRows}" selected="{SelectedMetaRuleIndex}"
|
||||||
|
|
@ -530,6 +539,9 @@
|
||||||
onclick="{CopyLootProfile}" />
|
onclick="{CopyLootProfile}" />
|
||||||
<button x="494" y="0" w="58" h="21" text="Clear"
|
<button x="494" y="0" w="58" h="21" text="Clear"
|
||||||
onclick="{ClearLootProfile}" />
|
onclick="{ClearLootProfile}" />
|
||||||
|
<button x="560" y="0" w="64" h="21" text="Delete"
|
||||||
|
onclick="{DeleteLootProfile}"
|
||||||
|
tooltip="Delete the selected named loot profile's real .utl file. Not available for By char." />
|
||||||
<button x="684" y="0" w="92" h="21" text="Back"
|
<button x="684" y="0" w="92" h="21" text="Back"
|
||||||
onclick="{CloseLootEditor}" />
|
onclick="{CloseLootEditor}" />
|
||||||
<list x="4" y="24" w="520" h="82" rowheight="17"
|
<list x="4" y="24" w="520" h="82" rowheight="17"
|
||||||
|
|
|
||||||
37
tests/AcDream.App.Tests/Plugins/VtankProfilesDefaultTests.cs
Normal file
37
tests/AcDream.App.Tests/Plugins/VtankProfilesDefaultTests.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
using AcDream.App.Plugins;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Plugins;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Item H (Campaign VT slice-1 fix round): a genuinely failable Linux-path
|
||||||
|
/// guarantee for <see cref="VtankProfilesDefault.Resolve"/> — re-derives the
|
||||||
|
/// expected value with <see cref="Path.Combine(string, string)"/> from an
|
||||||
|
/// injected root and compares, so a future hard-coded
|
||||||
|
/// <c>dataDirectory + "\vtank"</c> (or any other literal-backslash
|
||||||
|
/// regression) fails this test on any OS, not just by inspection.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class VtankProfilesDefaultTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ResolveIsBuiltWithPathCombineOnly()
|
||||||
|
{
|
||||||
|
const string injectedRoot = "C:/some/injected/data-dir";
|
||||||
|
string expected = Path.Combine(injectedRoot, "vtank");
|
||||||
|
|
||||||
|
string actual = VtankProfilesDefault.Resolve(injectedRoot);
|
||||||
|
|
||||||
|
Assert.Equal(expected, actual);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
injectedRoot + "\\vtank",
|
||||||
|
actual.Replace(Path.DirectorySeparatorChar, '/'),
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveAppendsVtankBeneathWhicheverRootIsInjected()
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
Path.Combine("/home/user/.local/share/acdream", "vtank"),
|
||||||
|
VtankProfilesDefault.Resolve("/home/user/.local/share/acdream"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -169,6 +169,7 @@ public sealed class RuntimeOptionsTests
|
||||||
Assert.False(opts.UiProbeEnabled);
|
Assert.False(opts.UiProbeEnabled);
|
||||||
Assert.False(opts.HasLiveCredentials);
|
Assert.False(opts.HasLiveCredentials);
|
||||||
Assert.Empty(opts.PluginTags);
|
Assert.Empty(opts.PluginTags);
|
||||||
|
Assert.Null(opts.VtankProfileDirectoryOverride);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -186,6 +187,26 @@ public sealed class RuntimeOptionsTests
|
||||||
Assert.Equal(["healer", "Leader", "scout"], options.PluginTags);
|
Assert.Equal(["healer", "Leader", "scout"], options.PluginTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VtankProfileDirectoryOverrideIsNullUnlessSet()
|
||||||
|
{
|
||||||
|
RuntimeOptions blank = RuntimeOptions.Parse(
|
||||||
|
AnyDatDir,
|
||||||
|
Env(new() { ["ACDREAM_VTANK_PROFILE_DIR"] = "" }));
|
||||||
|
Assert.Null(blank.VtankProfileDirectoryOverride);
|
||||||
|
|
||||||
|
RuntimeOptions set = RuntimeOptions.Parse(
|
||||||
|
AnyDatDir,
|
||||||
|
Env(new()
|
||||||
|
{
|
||||||
|
["ACDREAM_VTANK_PROFILE_DIR"] =
|
||||||
|
"C:/Games/VirindiPlugins/VirindiTank",
|
||||||
|
}));
|
||||||
|
Assert.Equal(
|
||||||
|
"C:/Games/VirindiPlugins/VirindiTank",
|
||||||
|
set.VtankProfileDirectoryOverride);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PreparedAssetPath_DefaultsBesideDats_AndAllowsOneOverride()
|
public void PreparedAssetPath_DefaultsBesideDats_AndAllowsOneOverride()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,26 @@ public sealed class PluginSessionTests
|
||||||
alpha.Storage.WriteText("../escape.json", "bad"));
|
alpha.Storage.WriteText("../escape.json", "bad"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Item F (Campaign VT slice-1 fix round): VtankProfiles must be
|
||||||
|
// forwarded UNSCOPED, unlike Storage — it names one shared external
|
||||||
|
// location (a real VTank profile folder, or a host-composed portable
|
||||||
|
// default), not per-plugin data, so prefixing it by manifest id would
|
||||||
|
// defeat the point of pointing it at a real installed VTank directory.
|
||||||
|
[Fact]
|
||||||
|
public void ScopedHostForwardsVtankProfilesUnscoped()
|
||||||
|
{
|
||||||
|
var vtankProfiles = new MemoryStorage();
|
||||||
|
using var scope = new ScopedPluginHost(
|
||||||
|
new StubHost(vtankProfiles: vtankProfiles),
|
||||||
|
"acdream.alpha",
|
||||||
|
"Alpha");
|
||||||
|
|
||||||
|
scope.VtankProfiles.WriteText("Shared.usd", "content");
|
||||||
|
|
||||||
|
Assert.Same(vtankProfiles, scope.VtankProfiles);
|
||||||
|
Assert.Equal("content", vtankProfiles.Text["Shared.usd"]);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ScopedHostNamespacesAndUnregistersLootClassifierOnDispose()
|
public void ScopedHostNamespacesAndUnregistersLootClassifierOnDispose()
|
||||||
{
|
{
|
||||||
|
|
@ -364,7 +384,8 @@ public sealed class PluginSessionTests
|
||||||
|
|
||||||
private sealed class StubHost(
|
private sealed class StubHost(
|
||||||
IPluginStorage? storage = null,
|
IPluginStorage? storage = null,
|
||||||
IPluginLootClassifierRegistry? lootClassifiers = null) : IPluginHost
|
IPluginLootClassifierRegistry? lootClassifiers = null,
|
||||||
|
IPluginStorage? vtankProfiles = null) : IPluginHost
|
||||||
{
|
{
|
||||||
public bool HasUi => false;
|
public bool HasUi => false;
|
||||||
public IPluginLogger Log { get; } = new StubLogger();
|
public IPluginLogger Log { get; } = new StubLogger();
|
||||||
|
|
@ -377,6 +398,8 @@ public sealed class PluginSessionTests
|
||||||
storage ?? NoOpPluginStorage.Instance;
|
storage ?? NoOpPluginStorage.Instance;
|
||||||
public IPluginLootClassifierRegistry LootClassifiers { get; } =
|
public IPluginLootClassifierRegistry LootClassifiers { get; } =
|
||||||
lootClassifiers ?? NoOpPluginLootClassifierRegistry.Instance;
|
lootClassifiers ?? NoOpPluginLootClassifierRegistry.Instance;
|
||||||
|
public IPluginStorage VtankProfiles { get; } =
|
||||||
|
vtankProfiles ?? NoOpPluginStorage.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class KeepClassifier : IPluginLootClassifier
|
private sealed class KeepClassifier : IPluginLootClassifier
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,7 @@
|
||||||
Link="mosstank.xml"
|
Link="mosstank.xml"
|
||||||
CopyToOutputDirectory="PreserveNewest" />
|
CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="Fixtures\vtank\**\*.*" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ public sealed class AttackSpellCatalogTests
|
||||||
IsProjectile = true,
|
IsProjectile = true,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
var settings = new CombatSettings { UseArcs = true, ArcRange = 15 };
|
var settings = new CombatSettings { UseArcs = UseArcsMode.AtRange, ArcRange = 15 };
|
||||||
var actions = new MonsterRuleActions
|
var actions = new MonsterRuleActions
|
||||||
{
|
{
|
||||||
DamageType = MonsterDamageType.Fire,
|
DamageType = MonsterDamageType.Fire,
|
||||||
|
|
@ -215,7 +215,7 @@ public sealed class AttackSpellCatalogTests
|
||||||
|
|
||||||
IReadOnlyList<AttackSpellChoice> choices = catalog.Candidates(
|
IReadOnlyList<AttackSpellChoice> choices = catalog.Candidates(
|
||||||
new MonsterRuleActions { DamageType = MonsterDamageType.Auto },
|
new MonsterRuleActions { DamageType = MonsterDamageType.Auto },
|
||||||
new CombatSettings { UseArcs = false },
|
new CombatSettings { UseArcs = UseArcsMode.No },
|
||||||
target,
|
target,
|
||||||
0,
|
0,
|
||||||
Character.Instance);
|
Character.Instance);
|
||||||
|
|
|
||||||
1036
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/af/aphus.af
Normal file
1036
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/af/aphus.af
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,559 @@
|
||||||
|
~~ {
|
||||||
|
~~ FOR AUTO-COMPLETION ASSISTANCE: testvar getvar setvar touchvar clearallvars clearvar getcharintprop getchardoubleprop getcharquadprop getcharboolprop getcharstringprop getisspellknown getcancastspell_hunt getcancastspell_buff getcharvital_base getcharvital_current getcharvital_buffedmax getcharskill_traininglevel getcharskill_base getcharskill_buffed getplayerlandcell getplayercoordinates coordinategetns coordinategetwe coordinategetz coordinatetostring coordinateparse coordinatedistancewithz coordinatedistanceflat wobjectgetphysicscoordinates wobjectgetname wobjectgetobjectclass wobjectgettemplatetype wobjectgetisdooropen wobjectfindnearestmonster wobjectfindnearestdoor wobjectfindnearestbyobjectclass wobjectfindininventorybytemplatetype wobjectfindininventorybyname wobjectfindininventorybynamerx wobjectgetselection wobjectgetplayer wobjectfindnearestbynameandobjectclass actiontryselect actiontryuseitem actiontryapplyitem actiontrygiveitem actiontryequipanywand actiontrycastbyid actiontrycastbyidontarget chatbox chatboxpaste statushud statushudcolored uigetcontrol uisetlabel isfalse istrue iif randint cstr strlen getobjectinternaltype cstrf stopwatchcreate stopwatchstart stopwatchstop stopwatchelapsedseconds cnumber floor ceiling round abs getworldname getitemcountininventorybyname getheading getitemcountininventorybynamerx getheadingto actiontrygiveprofile vitae getfellowshipstatus getfellowshipname getfellowshipisopen getfellowshipisleader getfellowshipleaderid getfellowshipcanrecruit getfellowid getfellowshipcount getfellowshiplocked getfellowname getfellowshipisfull sin cos tan sqrt asin acos atan atan2 sinh cosh tanh vtsetmetastate getregexmatch echo chr ord wobjectgetid wobjectgethealth wobjectfindbyid wobjectgetintprop wobjectfindnearestbytemplatetype wobjectgetopencontainer testquestflag getquestktprogress isrefreshingquests getquestktrequired getqueststatus getisday getgamehour getgamehourname getisnight getgameday getgameticks getminutesuntilday getgamemonth getgamemonthname getminutesuntilnight getgameyear uisetvisible uiviewvisible uiviewexists getgvar touchgvar getpvar touchpvar setgvar cleargvar setpvar clearpvar testgvar clearallgvars testpvar clearallpvars dictgetitem dictcreate dicthaskey dictadditem dictkeys dictremovekey dictvalues dictclear dictsize dictcopy listgetitem listpop listcreate listcontains listremove listadd listindexof listremoveat listinsert listlastindexof listclear listcopy listcount listreverse
|
||||||
|
|
||||||
|
~~
|
||||||
|
~~ File auto-generated by metaf, a program created by Eskarina of Morningthaw/Coldeve.
|
||||||
|
~~ Get metaf here: https://github.com/JJEII/metaf/
|
||||||
|
~~
|
||||||
|
~~ All recognized structural designators:
|
||||||
|
~~ STATE: DO:
|
||||||
|
~~ IF: NAV:
|
||||||
|
~~
|
||||||
|
~~ All recognized CONDITION (IF:) operation keywords:
|
||||||
|
~~ Never NavEmpty MobsInDist_Priority Not
|
||||||
|
~~ Always Death NeedToBuff PSecsInStateGE
|
||||||
|
~~ All VendorOpen NoMobsInDist SecsOnSpellGE
|
||||||
|
~~ Any VendorClosed BlockE BuPercentGE
|
||||||
|
~~ ChatMatch ItemCountLE CellE DistToRteGE
|
||||||
|
~~ MainSlotsLE ItemCountGE IntoPortal Expr
|
||||||
|
~~ SecsInStateGE MobsInDist_Name ExitPortal ChatCapture
|
||||||
|
~~
|
||||||
|
~~ All recognized ACTION (DO:) operation keywords:
|
||||||
|
~~ None EmbedNav ChatExpr SetOpt
|
||||||
|
~~ SetState CallState SetWatchdog CreateView
|
||||||
|
~~ Chat Return ClearWatchdog DestroyView
|
||||||
|
~~ DoAll DoExpr GetOpt DestroyAllViews
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV types:
|
||||||
|
~~ circular follow
|
||||||
|
~~ linear once
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV NODE types:
|
||||||
|
~~ flw vnd
|
||||||
|
~~ pnt ptl
|
||||||
|
~~ rcl tlk
|
||||||
|
~~ pau chk
|
||||||
|
~~ cht jmp
|
||||||
|
~~ prt (deprecated in VTank)
|
||||||
|
~~ }
|
||||||
|
|
||||||
|
STATE: {Default} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DestroyAllViews
|
||||||
|
IF: Always
|
||||||
|
DO: CreateView {main} {<?xml version="1.0"?><view width="370" height="200" title="XP Augmentation Categories"><control type="layout"><control type="button" name="btnBurdenPackSlots" left="10" top="10" width="170" height="15" text="Burden and Pack Slots" setstate="stateBurdenPackSlots" /><control type="button" name="btnDeathPenalties" left="10" top="40" width="170" height="15" text="Death Penalties" setstate="stateDeathPenalties" /><control type="button" name="btnExperienceBonus" left="10" top="70" width="170" height="15" text="Experience Bonus" setstate="stateExperienceBonus" /><control type="button" name="btnHealthArmorRegen" left="10" top="100" width="170" height="15" text="Health, Armor, and Regeneration" setstate="stateHealthArmorRegen" /><control type="button" name="btnInnateAttribute" left="10" top="130" width="170" height="15" text="Innate Attribute" setstate="stateInnateAttribute" /><control type="button" name="btnStipend" left="10" top="160" width="170" height="15" text="Stipend" setstate="stateStipend" /><control type="button" name="btnInnateResistance" left="190" top="10" width="170" height="15" text="Innate Resistance" setstate="stateInnateResistance" /><control type="button" name="btnRating" left="190" top="40" width="170" height="15" text="Rating" setstate="stateRatings" /><control type="button" name="btnSalvagingTinkering" left="190" top="70" width="170" height="15" text="Salvaging and Tinkering" setstate="stateSalvagingTinkering" /><control type="button" name="btnSkills" left="190" top="100" width="170" height="15" text="Skills" setstate="stateSkills" /><control type="button" name="btnSpellDuration" left="190" top="130" width="170" height="15" text="Spell Duration" setstate="stateSpellDuration" /><control type="button" name="btnBella" left="190" top="160" width="170" height="15" text="Bella" setstate="stateBella" /></control></view>}
|
||||||
|
~~ }
|
||||||
|
STATE: {dummy} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DestroyView {main}
|
||||||
|
IF: Always
|
||||||
|
DO: CreateView {state} {}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateBurdenPackSlots} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DestroyView {main}
|
||||||
|
IF: Always
|
||||||
|
DO: CreateView {stateBurdenPackSlots} {<?xml version="1.0"?><view width="300" height="260" title="Burden and Pack Slots"><control type="layout"><control type="button" name="btnMightSeventhMule" left="10" top="10" width="250" height="25" text="Might of the Seventh Mule" actionexpr="setvar[Augment,`stateMightSeventhMule`]" setstate="nav_zaikhal" /><control type="button" name="btnShadowSeventhMule" left="10" top="40" width="250" height="25" text="Shadow of the Seventh Mule" actionexpr="setvar[Augment,`stateShadowSeventhMule`]" setstate="nav_zaikhal" /><control type="button" name="btnInfusedWarMagic" left="10" top="70" width="250" height="25" text="Infused War Magic" actionexpr="setvar[Augment,`stateInfusedWarMagic`]" setstate="nav_silyun" /><control type="button" name="btnInfusedLifeMagic" left="10" top="100" width="250" height="25" text="Infused Life Magic" actionexpr="setvar[Augment,`stateInfusedLifeMagic`]" setstate="nav_hebianto" /><control type="button" name="btnInfusedItemMagic" left="10" top="130" width="250" height="25" text="Infused Item Magic" actionexpr="setvar[Augment,`stateInfusedItemMagic`]" setstate="nav_hebianto" /><control type="button" name="btnInfusedCreatureMagic" left="10" top="160" width="250" height="25" text="Infused Creature Magic" actionexpr="setvar[Augment,`stateInfusedCreatureMagic`]" setstate="nav_cragstone" /><control type="button" name="btnInfusedVoidMagic" left="10" top="190" width="250" height="25" text="Infused Void Magic" actionexpr="setvar[Augment,`stateInfusedVoidMagic`]" setstate="nav_candeth" /><control type="button" name="btnMain" left="10" top="220" width="250" height="25" text="Main" setstate="Default" /></control></view>}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateDeathPenalties} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DestroyView {main}
|
||||||
|
IF: Always
|
||||||
|
DO: CreateView {stateDeathPenalties} {<?xml version="1.0"?><view width="300" height="110" title="Death Penalties"><control type="layout"><control type="button" name="btnClutchMiser" left="10" top="10" width="250" height="25" text="Clutch of the Miser" actionexpr="setvar[Augment,`stateClutchMiser`]" setstate="nav_ab" /><control type="button" name="btnEnduringEnchantment" left="10" top="40" width="250" height="25" text="Enduring Enchantment" actionexpr="setvar[Augment,`stateEnduringEnchantment`]" setstate="nav_ab" /><control type="button" name="btnMain" left="10" top="70" width="250" height="25" text="Main" setstate="Default" /></control></view>}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateExperienceBonus} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DestroyView {main}
|
||||||
|
IF: Always
|
||||||
|
DO: CreateView {stateExperienceBonus} {<?xml version="1.0"?><view width="300" height="110" title="Experience Bonus"><control type="layout"><control type="button" name="btnQuickLearner" left="10" top="10" width="250" height="25" text="Quick Learner" actionexpr="setvar[Augment,`stateQuickLearner`]" setstate="nav_silyun" /><control type="button" name="btnMain" left="10" top="40" width="250" height="25" text="Main" setstate="Default" /></control></view>}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateHealthArmorRegen} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DestroyView {main}
|
||||||
|
IF: Always
|
||||||
|
DO: CreateView {stateHealthArmorRegen} {<?xml version="1.0"?><view width="300" height="110" title="Health, Armor, and Regeneration"><control type="layout"><control type="button" name="btnAsheronsLesserBenediction" left="10" top="10" width="250" height="25" text="Asheron's Lesser Benediction" actionexpr="setvar[Augment,`stateAsheronsLesserBenediction`]" setstate="nav_silyun" /><control type="button" name="btnInnateRenewal" left="10" top="40" width="250" height="25" text="Innate Renewal" actionexpr="setvar[Augment,`stateInnateRenewal`]" setstate="nav_bandit" /><control type="button" name="btnMain" left="10" top="70" width="250" height="25" text="Main" setstate="Default" /></control></view>}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateInnateAttribute} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DestroyView {main}
|
||||||
|
IF: Always
|
||||||
|
DO: CreateView {stateInnateAttribute} {<?xml version="1.0"?><view width="300" height="220" title="Innate Attribute"><control type="layout"><control type="button" name="btnReinforcementLugians" left="10" top="10" width="250" height="25" text="Reinforcement of the Lugians" actionexpr="setvar[Augment,`stateReinforcementLugians`]" setstate="nav_fiun" /><control type="button" name="btnBleearghFortitude" left="10" top="40" width="250" height="25" text="Bleeargh's Fortitude" actionexpr="setvar[Augment,`stateBleearghFortitude`]" setstate="nav_fiun" /><control type="button" name="btnOswaldEnhancement" left="10" top="70" width="250" height="25" text="Oswald's Enhancement" actionexpr="setvar[Augment,`stateOswaldEnhancement`]" setstate="nav_fiun" /><control type="button" name="btnSiraluunBlessing" left="10" top="100" width="250" height="25" text="Siraluun's Blessing" actionexpr="setvar[Augment,`stateSiraluunBlessing`]" setstate="nav_fiun" /><control type="button" name="btnEnduringCalm" left="10" top="130" width="250" height="25" text="Enduring Calm" actionexpr="setvar[Augment,`stateEnduringCalm`]" setstate="nav_fiun" /><control type="button" name="btnSteadfastWill" left="10" top="160" width="250" height="25" text="Steadfast Will" actionexpr="setvar[Augment,`stateSteadfastWill`]" setstate="nav_fiun" /><control type="button" name="btnMain" left="10" top="190" width="250" height="25" text="Main" setstate="Default" /></control></view>}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateInnateResistance} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DestroyView {main}
|
||||||
|
IF: Always
|
||||||
|
DO: CreateView {stateInnateResistance} {<?xml version="1.0"?><view width="300" height="250" title="Innate Resistance"><control type="layout"><control type="button" name="btnEnhancementMaceTurner" left="10" top="10" width="250" height="25" text="Enhancement of the Mace Turner" actionexpr="setvar[Augment,`stateEnhancementMaceTurner`]" setstate="nav_hebianto" /><control type="button" name="btnEnhancementBladeTurner" left="10" top="40" width="250" height="25" text="Enhancement of the Blade Turner" actionexpr="setvar[Augment,`stateEnhancementBladeTurner`]" setstate="nav_hebianto" /><control type="button" name="btnEnhancementArrowTurner" left="10" top="70" width="250" height="25" text="Enhancement of the Arrow Turner" actionexpr="setvar[Augment,`stateEnhancementArrowTurner`]" setstate="nav_hebianto" /><control type="button" name="btnStormEnhancement" left="10" top="100" width="250" height="25" text="Storm's Enhancement" actionexpr="setvar[Augment,`stateStormEnhancement`]" setstate="nav_hebianto" /><control type="button" name="btnFieryEnhancement" left="10" top="130" width="250" height="25" text="Fiery Enhancement" actionexpr="setvar[Augment,`stateFieryEnhancement`]" setstate="nav_hebianto" /><control type="button" name="btnIcyEnhancement" left="10" top="160" width="250" height="25" text="Icy Enhancement" actionexpr="setvar[Augment,`stateIcyEnhancement`]" setstate="nav_hebianto" /><control type="button" name="btnCausticEnhancement" left="10" top="190" width="250" height="25" text="Caustic Enhancement" actionexpr="setvar[Augment,`stateCausticEnhancement`]" setstate="nav_hebianto" /><control type="button" name="btnMain" left="10" top="220" width="250" height="25" text="Main" setstate="Default" /></control></view>}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateRatings} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DestroyView {main}
|
||||||
|
IF: Always
|
||||||
|
DO: CreateView {stateRatings} {<?xml version="1.0"?><view width="300" height="180" title="Ratings"><control type="layout"><control type="button" name="btnCriticalProtection" left="10" top="10" width="250" height="25" text="Critical Protection" actionexpr="setvar[Augment,`stateCriticalProtection`]" setstate="nav_sanamar" /><control type="button" name="btnFrenzySlayer" left="10" top="40" width="250" height="25" text="Frenzy of the Slayer" actionexpr="setvar[Augment,`stateFrenzySlayer`]" setstate="nav_bandit" /><control type="button" name="btnIronSkin" left="10" top="70" width="250" height="25" text="Iron Skin of the Invincible" actionexpr="setvar[Augment,`stateIronSkin`]" setstate="nav_cragstone" /><control type="button" name="btnEyeRemorseless" left="10" top="100" width="250" height="25" text="Eye of the Remorseless" actionexpr="setvar[Augment,`stateEyeRemorseless`]" setstate="nav_ab" /><control type="button" name="btnHandRemorseless" left="10" top="130" width="250" height="25" text="Hand of the Remorseless" actionexpr="setvar[Augment,`stateHandRemorseless`]" setstate="nav_ab" /><control type="button" name="btnMain" left="10" top="160" width="250" height="25" text="Main" setstate="Default" /></control></view>}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateSalvagingTinkering} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DestroyView {main}
|
||||||
|
IF: Always
|
||||||
|
DO: CreateView {stateSalvagingTinkering} {<?xml version="1.0"?><view width="300" height="310" title="Salvaging and Tinkering"><control type="layout"><control type="button" name="btnCiandrasFortune" left="10" top="10" width="250" height="25" text="Ciandra's Fortune" actionexpr="setvar[Augment,`stateCiandrasFortune`]" setstate="nav_cragstone" /><control type="button" name="btnCharmedSmith" left="10" top="40" width="250" height="25" text="Charmed Smith" actionexpr="setvar[Augment,`stateCharmedSmith`]" setstate="nav_oolutangasrefuge" /><control type="button" name="btnJibrilsEssence" left="10" top="70" width="250" height="25" text="Jibril's Essence" actionexpr="setvar[Augment,`stateJibrilsEssence`]" setstate="nav_cragstone" /><control type="button" name="btnYoshisEssence" left="10" top="100" width="250" height="25" text="Yoshi's Essence" actionexpr="setvar[Augment,`stateYoshisEssence`]" setstate="nav_cragstone" /><control type="button" name="btnCeldisethsEssence" left="10" top="130" width="250" height="25" text="Celdiseth's Essence" actionexpr="setvar[Augment,`stateCeldisethsEssence`]" setstate="nav_cragstone" /><control type="button" name="btnKogasEssence" left="10" top="160" width="250" height="25" text="Koga's Essence" actionexpr="setvar[Augment,`stateKogasEssence`]" setstate="nav_cragstone" /><control type="button" name="btnCiandrasEssence" left="10" top="190" width="250" height="25" text="Ciandra's Essence" actionexpr="setvar[Augment,`stateCiandrasEssence`]" setstate="nav_cragstone" /><control type="button" name="btnMain" left="10" top="220" width="250" height="25" text="Main" setstate="Default" /></control></view>}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateSkills} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DestroyView {main}
|
||||||
|
IF: Always
|
||||||
|
DO: CreateView {stateSkills} {<?xml version="1.0"?><view width="300" height="180" title="Skills"><control type="layout"><control type="button" name="btnMasterSteelCircle" left="10" top="10" width="250" height="25" text="Master of the Steel Circle" actionexpr="setvar[Augment,`stateMasterSteelCircle`]" setstate="nav_silyun" /><control type="button" name="btnMasterFiveFoldPath" left="10" top="40" width="250" height="25" text="Master of the Five Fold Path" actionexpr="setvar[Augment,`stateMasterFiveFoldPath`]" setstate="nav_zaikhal" /><control type="button" name="btnMasterFocusedEye" left="10" top="70" width="250" height="25" text="Master of the Focused Eye" actionexpr="setvar[Augment,`stateMasterFocusedEye`]" setstate="nav_zaikhal" /><control type="button" name="btnJackTrades" left="10" top="100" width="250" height="25" text="Jack of All Trades" actionexpr="setvar[Augment,`stateJackTrades`]" setstate="nav_bandit" /><control type="button" name="btnMain" left="10" top="130" width="250" height="25" text="Main" setstate="Default" /></control></view>}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateSpellDuration} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DestroyView {main}
|
||||||
|
IF: Always
|
||||||
|
DO: CreateView {stateSpellDuration} {<?xml version="1.0"?><view width="300" height="110" title="Spell Duration"><control type="layout"><control type="button" name="btnArchmageEndurance" left="10" top="10" width="250" height="25" text="Archmage's Endurance" actionexpr="setvar[Augment,`stateArchmageEndurance`]" setstate="nav_mayoi" /><control type="button" name="btnMain" left="10" top="40" width="250" height="25" text="Main" setstate="Default" /></control></view>}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateMightSeventhMule} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_husoon}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateShadowSeventhMule} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_dumidabintruminre}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateInfusedWarMagic} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_rapheldetante}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateArchmageEndurance} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_nawamaraujio}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateInfusedLifeMagic} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_akemifei}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateInfusedItemMagic} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_ganfo}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateInfusedCreatureMagic} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_gustuvlansdown}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateInfusedVoidMagic} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_morathe}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateClutchMiser} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_rohulabintludun}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateEnduringEnchantment} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_erikfestus}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateQuickLearner} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_rickarddumalia}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateAsheronsLesserBenediction} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_donatellolinante}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateInnateRenewal} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_alisondulane}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateReinforcementLugians} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_fiunluunere}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateBleearghFortitude} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_fiunruun}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateOswaldEnhancement} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_fiunbayaas}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateSiraluunBlessing} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_fiunriish}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateEnduringCalm} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_fiunvasherr}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateSteadfastWill} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_fiunnoress}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateCriticalProtection} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_piersantilinante}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateFrenzySlayer} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_neelanashua}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateIronSkin} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_emilyyarow}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateEyeRemorseless} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_anframmellow}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateHandRemorseless} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_alishiabintaldan}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateEnhancementMaceTurner} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_nawamaradia}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateEnhancementBladeTurner} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_ilinwis}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateEnhancementArrowTurner} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_kyujorujen}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateStormEnhancement} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_enliyuo}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateFieryEnhancement} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_rikshenri}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateIcyEnhancement} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_lubao}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateCausticEnhancement} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_shujiomilao}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {nav_zaikhal} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_zaikhal}
|
||||||
|
IF: Always
|
||||||
|
DO: DestroyAllViews
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoExpr {chatbox[\/vt setmetastate +getvar[Augment]]}
|
||||||
|
~~ }
|
||||||
|
STATE: {nav_fiun} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_fiun}
|
||||||
|
IF: Always
|
||||||
|
DO: DestroyAllViews
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoExpr {chatbox[\/vt setmetastate +getvar[Augment]]}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateCiandrasFortune} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_kriscennis}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateCharmedSmith} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_lug}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateJibrilsEssence} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_joshunfelden}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateYoshisEssence} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_briennecarlus}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateCeldisethsEssence} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_burrellsammrun}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateKogasEssence} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_lenorturk}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateCiandrasEssence} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_robertcrow}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateMasterSteelCircle} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_carlitogallo}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateMasterFiveFoldPath} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_rahinabintzalanis}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateMasterFocusedEye} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_kilaf}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateJackTrades} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_ariannatheadept}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {nav_silyun} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_silyun}
|
||||||
|
IF: Always
|
||||||
|
DO: DestroyAllViews
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoExpr {chatbox[\/vt setmetastate +getvar[Augment]]}
|
||||||
|
~~ }
|
||||||
|
STATE: {nav_hebianto} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_hebianto}
|
||||||
|
IF: Always
|
||||||
|
DO: DestroyAllViews
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoExpr {chatbox[\/vt setmetastate +getvar[Augment]]}
|
||||||
|
~~ }
|
||||||
|
STATE: {nav_cragstone} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_cragstone}
|
||||||
|
IF: Always
|
||||||
|
DO: DestroyAllViews
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoExpr {chatbox[\/vt setmetastate +getvar[Augment]]}
|
||||||
|
~~ }
|
||||||
|
STATE: {nav_oolutangasrefuge} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_oolutangasrefuge}
|
||||||
|
IF: Always
|
||||||
|
DO: DestroyAllViews
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoExpr {chatbox[\/vt setmetastate +getvar[Augment]]}
|
||||||
|
~~ }
|
||||||
|
STATE: {nav_ab} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_ab}
|
||||||
|
IF: Always
|
||||||
|
DO: DestroyAllViews
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoExpr {chatbox[\/vt setmetastate +getvar[Augment]]}
|
||||||
|
~~ }
|
||||||
|
STATE: {nav_bandit} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_bandit}
|
||||||
|
IF: Always
|
||||||
|
DO: DestroyAllViews
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoExpr {chatbox[\/vt setmetastate +getvar[Augment]]}
|
||||||
|
~~ }
|
||||||
|
STATE: {nav_sanamar} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_sanamar}
|
||||||
|
IF: Always
|
||||||
|
DO: DestroyAllViews
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoExpr {chatbox[\/vt setmetastate +getvar[Augment]]}
|
||||||
|
~~ }
|
||||||
|
STATE: {nav_candeth} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_candeth}
|
||||||
|
IF: Always
|
||||||
|
DO: DestroyAllViews
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoExpr {chatbox[\/vt setmetastate +getvar[Augment]]}
|
||||||
|
~~ }
|
||||||
|
STATE: {nav_mayoi} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load nav_mayoi}
|
||||||
|
IF: Always
|
||||||
|
DO: DestroyAllViews
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoExpr {chatbox[\/vt setmetastate +getvar[Augment]]}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateBella} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {echo[`OK RUNNING BELLA`,13]}
|
||||||
|
DestroyView {main}
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: SetState {mp_primary}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/mp}
|
||||||
|
~~ }
|
||||||
|
STATE: {jaw_hunt} ~~ {
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
SetState {turn_in_jaw}
|
||||||
|
IF: ItemCountGE 1 {Insatiable Eater Jaw}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load to_fiun}
|
||||||
|
~~ }
|
||||||
|
STATE: {jaw_run} ~~ {
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load jaw_1_hunt}
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
Chat {/vt opt set enablelooting true}
|
||||||
|
SetState {jaw_hunt}
|
||||||
|
~~ }
|
||||||
|
STATE: {kill_bella} ~~ {
|
||||||
|
IF: ItemCountGE 1 {Blank Augmentation Gem}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load recallls}
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {mp_primary} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load mp_primary}
|
||||||
|
Chat {/vt opt set enablebuffing true}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
Chat {/vt opt set NavPriorityBoost false}
|
||||||
|
Chat {/vt opt set navclosestoprange 0.008}
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load jaw_1}
|
||||||
|
SetState {jaw_run}
|
||||||
|
~~ }
|
||||||
|
STATE: {mp_secondary} ~~ {
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load to_bella}
|
||||||
|
SetState {kill_bella}
|
||||||
|
~~ }
|
||||||
|
STATE: {mp_trans} ~~ {
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load mp_secondary}
|
||||||
|
SetState {mp_secondary}
|
||||||
|
~~ }
|
||||||
|
STATE: {turn_in_jaw} ~~ {
|
||||||
|
IF: ChatMatch {^Fiun Rehlyun.*}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/mp}
|
||||||
|
SetState {mp_trans}
|
||||||
|
~~ }
|
||||||
|
STATE: {stateStipend} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {echo[`OK RUNNING STIPEND`,13]}
|
||||||
|
DestroyView {main}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
IF: Always
|
||||||
|
DO: EmbedNav nav0__stipend_nav {stipend.nav}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
|
||||||
|
~~========================= ONLY NAVS APPEAR BELOW THIS LINE =========================~~
|
||||||
|
|
||||||
|
NAV: nav0__stipend_nav once ~~ {
|
||||||
|
cht -101.597905190786 -96.6216093699137 2.08333134651184E-05 {/ah}
|
||||||
|
pnt 59.3590666453044 -28.7057823816935 0.0500208298365275
|
||||||
|
pnt 59.3771680514018 -28.7152192115784 0.0500208298365275
|
||||||
|
ptl -101.597905190786 -96.6216093699137 2.08333134651184E-05 59.3936458587647 -28.7256083488464 0.0508250035345554 14 {Portal to Town Network}
|
||||||
|
pnt -101.615851815542 -96.6388638178507 2.08333134651184E-05
|
||||||
|
pnt -101.657751337687 -96.5832635879517 2.08333134651184E-05
|
||||||
|
pnt -101.658352184296 -96.5325949986776 2.08333134651184E-05
|
||||||
|
pnt -101.605949529012 -96.519695186615 2.08333134651184E-05
|
||||||
|
ptl -101.597905190786 -96.6216093699137 2.08333134651184E-05 -101.588099161784 -96.5166525046031 -0.000262499845121056 14 {Portal to Arwic}
|
||||||
|
pnt 56.6498762130737 33.416518386205 0.175020837783813
|
||||||
|
pnt 56.655900033315 33.5368880271912 0.175020837783813
|
||||||
|
pnt 56.7470087051392 33.5495386441549 0.175020837783813
|
||||||
|
pnt 56.7795230229696 33.6337207794189 0.175020837783813
|
||||||
|
tlk -101.597905190786 -96.6216093699137 2.08333134651184E-05 56.7816291809082 33.6455291748047 0.175020843744278 37 {Monroe}
|
||||||
|
~~ }
|
||||||
101
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/af/bella.af
Normal file
101
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/af/bella.af
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
~~ {
|
||||||
|
~~ FOR AUTO-COMPLETION ASSISTANCE: testvar getvar setvar touchvar clearallvars clearvar getcharintprop getchardoubleprop getcharquadprop getcharboolprop getcharstringprop getisspellknown getcancastspell_hunt getcancastspell_buff getcharvital_base getcharvital_current getcharvital_buffedmax getcharskill_traininglevel getcharskill_base getcharskill_buffed getplayerlandcell getplayercoordinates coordinategetns coordinategetwe coordinategetz coordinatetostring coordinateparse coordinatedistancewithz coordinatedistanceflat wobjectgetphysicscoordinates wobjectgetname wobjectgetobjectclass wobjectgettemplatetype wobjectgetisdooropen wobjectfindnearestmonster wobjectfindnearestdoor wobjectfindnearestbyobjectclass wobjectfindininventorybytemplatetype wobjectfindininventorybyname wobjectfindininventorybynamerx wobjectgetselection wobjectgetplayer wobjectfindnearestbynameandobjectclass actiontryselect actiontryuseitem actiontryapplyitem actiontrygiveitem actiontryequipanywand actiontrycastbyid actiontrycastbyidontarget chatbox chatboxpaste statushud statushudcolored uigetcontrol uisetlabel isfalse istrue iif randint cstr strlen getobjectinternaltype cstrf stopwatchcreate stopwatchstart stopwatchstop stopwatchelapsedseconds cnumber floor ceiling round abs getworldname getitemcountininventorybyname getheading getitemcountininventorybynamerx getheadingto actiontrygiveprofile vitae getfellowshipstatus getfellowshipname getfellowshipisopen getfellowshipisleader getfellowshipleaderid getfellowshipcanrecruit getfellowid getfellowshipcount getfellowshiplocked getfellowname getfellowshipisfull sin cos tan sqrt asin acos atan atan2 sinh cosh tanh vtsetmetastate getregexmatch echo chr ord wobjectgetid wobjectgethealth wobjectfindbyid wobjectgetintprop wobjectfindnearestbytemplatetype wobjectgetopencontainer testquestflag getquestktprogress isrefreshingquests getquestktrequired getqueststatus getisday getgamehour getgamehourname getisnight getgameday getgameticks getminutesuntilday getgamemonth getgamemonthname getminutesuntilnight getgameyear uisetvisible uiviewvisible uiviewexists getgvar touchgvar getpvar touchpvar setgvar cleargvar setpvar clearpvar testgvar clearallgvars testpvar clearallpvars dictgetitem dictcreate dicthaskey dictadditem dictkeys dictremovekey dictvalues dictclear dictsize dictcopy listgetitem listpop listcreate listcontains listremove listadd listindexof listremoveat listinsert listlastindexof listclear listcopy listcount listreverse
|
||||||
|
|
||||||
|
~~
|
||||||
|
~~ File auto-generated by metaf, a program created by Eskarina of Morningthaw/Coldeve.
|
||||||
|
~~ Get metaf here: https://github.com/JJEII/metaf/
|
||||||
|
~~
|
||||||
|
~~ All recognized structural designators:
|
||||||
|
~~ STATE: DO:
|
||||||
|
~~ IF: NAV:
|
||||||
|
~~
|
||||||
|
~~ All recognized CONDITION (IF:) operation keywords:
|
||||||
|
~~ Never NavEmpty MobsInDist_Priority Not
|
||||||
|
~~ Always Death NeedToBuff PSecsInStateGE
|
||||||
|
~~ All VendorOpen NoMobsInDist SecsOnSpellGE
|
||||||
|
~~ Any VendorClosed BlockE BuPercentGE
|
||||||
|
~~ ChatMatch ItemCountLE CellE DistToRteGE
|
||||||
|
~~ MainSlotsLE ItemCountGE IntoPortal Expr
|
||||||
|
~~ SecsInStateGE MobsInDist_Name ExitPortal ChatCapture
|
||||||
|
~~
|
||||||
|
~~ All recognized ACTION (DO:) operation keywords:
|
||||||
|
~~ None EmbedNav ChatExpr SetOpt
|
||||||
|
~~ SetState CallState SetWatchdog CreateView
|
||||||
|
~~ Chat Return ClearWatchdog DestroyView
|
||||||
|
~~ DoAll DoExpr GetOpt DestroyAllViews
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV types:
|
||||||
|
~~ circular follow
|
||||||
|
~~ linear once
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV NODE types:
|
||||||
|
~~ flw vnd
|
||||||
|
~~ pnt ptl
|
||||||
|
~~ rcl tlk
|
||||||
|
~~ pau chk
|
||||||
|
~~ cht jmp
|
||||||
|
~~ prt (deprecated in VTank)
|
||||||
|
~~ }
|
||||||
|
|
||||||
|
STATE: {Default} ~~ {
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: SetState {mp_primary}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/mp}
|
||||||
|
~~ }
|
||||||
|
STATE: {jaw_hunt} ~~ {
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
SetState {turn_in_jaw}
|
||||||
|
IF: ItemCountGE 1 {Insatiable Eater Jaw}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load to_fiun}
|
||||||
|
~~ }
|
||||||
|
STATE: {jaw_run} ~~ {
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load jaw_1_hunt}
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
Chat {/vt opt set enablelooting true}
|
||||||
|
SetState {jaw_hunt}
|
||||||
|
~~ }
|
||||||
|
STATE: {kill_bella} ~~ {
|
||||||
|
IF: ChatCapture {You have solved this quest too recently!} {}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load recallls}
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
~~ }
|
||||||
|
STATE: {mp_primary} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load mp_primary}
|
||||||
|
Chat {/vt opt set enablebuffing true}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
Chat {/vt opt set NavPriorityBoost false}
|
||||||
|
Chat {/vt opt set navclosestoprange 0.008}
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load jaw_1}
|
||||||
|
SetState {jaw_run}
|
||||||
|
~~ }
|
||||||
|
STATE: {mp_secondary} ~~ {
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load to_bella}
|
||||||
|
SetState {kill_bella}
|
||||||
|
~~ }
|
||||||
|
STATE: {mp_trans} ~~ {
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load mp_secondary}
|
||||||
|
SetState {mp_secondary}
|
||||||
|
~~ }
|
||||||
|
STATE: {turn_in_jaw} ~~ {
|
||||||
|
IF: ChatMatch {^Fiun Rehlyun.*}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/mp}
|
||||||
|
SetState {mp_trans}
|
||||||
|
~~ }
|
||||||
|
|
@ -0,0 +1,964 @@
|
||||||
|
~~ {
|
||||||
|
~~ FOR AUTO-COMPLETION ASSISTANCE: testvar getvar setvar touchvar clearallvars clearvar getcharintprop getchardoubleprop getcharquadprop getcharboolprop getcharstringprop getisspellknown getcancastspell_hunt getcancastspell_buff getcharvital_base getcharvital_current getcharvital_buffedmax getcharskill_traininglevel getcharskill_base getcharskill_buffed getplayerlandcell getplayercoordinates coordinategetns coordinategetwe coordinategetz coordinatetostring coordinateparse coordinatedistancewithz coordinatedistanceflat wobjectgetphysicscoordinates wobjectgetname wobjectgetobjectclass wobjectgettemplatetype wobjectgetisdooropen wobjectfindnearestmonster wobjectfindnearestdoor wobjectfindnearestbyobjectclass wobjectfindininventorybytemplatetype wobjectfindininventorybyname wobjectfindininventorybynamerx wobjectgetselection wobjectgetplayer wobjectfindnearestbynameandobjectclass actiontryselect actiontryuseitem actiontryapplyitem actiontrygiveitem actiontryequipanywand actiontrycastbyid actiontrycastbyidontarget chatbox chatboxpaste statushud statushudcolored uigetcontrol uisetlabel isfalse istrue iif randint cstr strlen getobjectinternaltype cstrf stopwatchcreate stopwatchstart stopwatchstop stopwatchelapsedseconds cnumber floor ceiling round abs getworldname getitemcountininventorybyname getheading getitemcountininventorybynamerx getheadingto actiontrygiveprofile vitae getfellowshipstatus getfellowshipname getfellowshipisopen getfellowshipisleader getfellowshipleaderid getfellowshipcanrecruit getfellowid getfellowshipcount getfellowshiplocked getfellowname getfellowshipisfull sin cos tan sqrt asin acos atan atan2 sinh cosh tanh vtsetmetastate getregexmatch echo chr ord wobjectgetid wobjectgethealth wobjectfindbyid wobjectgetintprop wobjectfindnearestbytemplatetype wobjectgetopencontainer testquestflag getquestktprogress isrefreshingquests getquestktrequired getqueststatus getisday getgamehour getgamehourname getisnight getgameday getgameticks getminutesuntilday getgamemonth getgamemonthname getminutesuntilnight getgameyear uisetvisible uiviewvisible uiviewexists getgvar touchgvar getpvar touchpvar setgvar cleargvar setpvar clearpvar testgvar clearallgvars testpvar clearallpvars dictgetitem dictcreate dicthaskey dictadditem dictkeys dictremovekey dictvalues dictclear dictsize dictcopy listgetitem listpop listcreate listcontains listremove listadd listindexof listremoveat listinsert listlastindexof listclear listcopy listcount listreverse
|
||||||
|
|
||||||
|
~~
|
||||||
|
~~ File auto-generated by metaf, a program created by Eskarina of Morningthaw/Coldeve.
|
||||||
|
~~ Get metaf here: https://github.com/JJEII/metaf/
|
||||||
|
~~
|
||||||
|
~~ All recognized structural designators:
|
||||||
|
~~ STATE: DO:
|
||||||
|
~~ IF: NAV:
|
||||||
|
~~
|
||||||
|
~~ All recognized CONDITION (IF:) operation keywords:
|
||||||
|
~~ Never NavEmpty MobsInDist_Priority Not
|
||||||
|
~~ Always Death NeedToBuff PSecsInStateGE
|
||||||
|
~~ All VendorOpen NoMobsInDist SecsOnSpellGE
|
||||||
|
~~ Any VendorClosed BlockE BuPercentGE
|
||||||
|
~~ ChatMatch ItemCountLE CellE DistToRteGE
|
||||||
|
~~ MainSlotsLE ItemCountGE IntoPortal Expr
|
||||||
|
~~ SecsInStateGE MobsInDist_Name ExitPortal ChatCapture
|
||||||
|
~~
|
||||||
|
~~ All recognized ACTION (DO:) operation keywords:
|
||||||
|
~~ None EmbedNav ChatExpr SetOpt
|
||||||
|
~~ SetState CallState SetWatchdog CreateView
|
||||||
|
~~ Chat Return ClearWatchdog DestroyView
|
||||||
|
~~ DoAll DoExpr GetOpt DestroyAllViews
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV types:
|
||||||
|
~~ circular follow
|
||||||
|
~~ linear once
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV NODE types:
|
||||||
|
~~ flw vnd
|
||||||
|
~~ pnt ptl
|
||||||
|
~~ rcl tlk
|
||||||
|
~~ pau chk
|
||||||
|
~~ cht jmp
|
||||||
|
~~ prt (deprecated in VTank)
|
||||||
|
~~ }
|
||||||
|
|
||||||
|
~~ /ub mexec setvar[navNumber,VILKETNUMMER]
|
||||||
|
STATE: {combat} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt opt set enablecombat true}
|
||||||
|
IF: SecsInStateGE 60
|
||||||
|
DO: SetState {ring_fail}
|
||||||
|
IF: NoMobsInDist 5
|
||||||
|
DO: SetState {hunt}
|
||||||
|
~~ }
|
||||||
|
STATE: {death} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load death_pause}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt opt set enablenav true}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {navbore}
|
||||||
|
~~ }
|
||||||
|
STATE: {hunt} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt opt set enablecombat false}
|
||||||
|
IF: Expr {getvar[lootTime] == 0}
|
||||||
|
DO: DoExpr {setvar[lootTime, 21600]}
|
||||||
|
IF: Not Expr {testvar[serviceCheckInterval]}
|
||||||
|
DO: DoExpr {setvar[serviceCheckInterval, 43200]}
|
||||||
|
IF: Not Expr {testvar[serviceClock]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[serviceClock,stopwatchcreate[]]}
|
||||||
|
DoExpr {stopwatchstart[getvar[serviceClock]]}
|
||||||
|
IF: Death
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a I died! But I will comeback!}
|
||||||
|
SetState {death}
|
||||||
|
IF: All
|
||||||
|
ItemCountLE 100 {Prismatic Taper}
|
||||||
|
NoMobsInDist 5
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a Out of tapers! Recomping.}
|
||||||
|
SetState {recomp}
|
||||||
|
IF: MainSlotsLE 4
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a Out of inventory slots! Recomping.}
|
||||||
|
SetState {recomp}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[serviceCheckInterval] > 0}
|
||||||
|
Expr {stopwatchelapsedseconds[getvar[serviceClock]]>=getvar[serviceCheckInterval]}
|
||||||
|
NoMobsInDist 5
|
||||||
|
DO: SetState {service_quest_refresh}
|
||||||
|
IF: Always
|
||||||
|
DO: SetWatchdog 3 120 {navbore}
|
||||||
|
IF: DistToRteGE 1000
|
||||||
|
DO: SetState {navbore}
|
||||||
|
IF: All
|
||||||
|
Expr {setvar[nearPortal, wobjectfindnearestbyobjectclass[14]]}
|
||||||
|
Expr {istrue[getvar[nearPortal]]}
|
||||||
|
Expr {coordinatedistancewithz[getplayercoordinates[], wobjectgetphysicscoordinates[getvar[nearPortal]]]<=4}
|
||||||
|
Not Expr {wobjectgetname[getvar[nearPortal]]==Singularity Caul}
|
||||||
|
DO: DoAll
|
||||||
|
ChatExpr {\/me is to close to +wobjectgetname[getvar[nearPortal]]}
|
||||||
|
ChatExpr {\/me at +coordinatetostring[getplayercoordinates[]]}
|
||||||
|
IF: All
|
||||||
|
Any
|
||||||
|
Expr {getvar[mobsClose] == 0}
|
||||||
|
Expr {getvar[mobsClose] == 1}
|
||||||
|
MobsInDist_Name 1 6 {}
|
||||||
|
DO: SetState {combat}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[mobsClose] == 2}
|
||||||
|
MobsInDist_Name 2 6 {}
|
||||||
|
DO: SetState {combat}
|
||||||
|
IF: All
|
||||||
|
Expr {stopwatchelapsedseconds[getvar[lootclock]]>getvar[lootTime]}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
SetState {loot}
|
||||||
|
IF: All
|
||||||
|
Expr {setvar[portal, wobjectfindnearestbyobjectclass[14]]}
|
||||||
|
Expr {istrue[getvar[portal]]}
|
||||||
|
Expr {coordinatedistancewithz[getplayercoordinates[], wobjectgetphysicscoordinates[getvar[portal]]] <= 8}
|
||||||
|
Not Expr {wobjectgetname[getvar[portal]]==Singularity Caul}
|
||||||
|
Not Expr {wobjectgetname[getvar[portal]]#Destroyed.*}
|
||||||
|
Expr {wobjectisvalid[getvar[portal]]}
|
||||||
|
Expr {setvar[portalHeading, getheadingto[getvar[portal]]]}
|
||||||
|
Expr {setvar[playerHeading, getheading[wobjectgetplayer[]]]}
|
||||||
|
Expr {setvar[relativeAngle, abs[getvar[portalHeading] - getvar[playerHeading]]]}
|
||||||
|
Expr {setvar[relativeAngle, iif[getvar[relativeAngle] > 180, 360 - getvar[relativeAngle], getvar[relativeAngle]]]}
|
||||||
|
Expr {setvar[turnDirection, iif[randint[0,2] == 0, -90, 90]]}
|
||||||
|
Expr {getvar[relativeAngle] <= 11.25}
|
||||||
|
Expr {setvar[newHeading, abs[getvar[playerHeading] + getvar[turnDirection]]]}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablenav false}
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
SetState {avoid_portal}
|
||||||
|
|
||||||
|
~~ }
|
||||||
|
STATE: {loot_rare} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablelooting true}
|
||||||
|
Chat {/vt opt set enablenav false}
|
||||||
|
IF: SecsInStateGE 30
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
SetState {hunt}
|
||||||
|
~~ }
|
||||||
|
STATE: {Default} ~~ {
|
||||||
|
IF: Not Expr {testvar[portalAvoidAngle]}
|
||||||
|
DO: DoExpr {setvar[portalAvoidAngle, 11.25]}
|
||||||
|
IF: Not Expr {testvar[augInnateStrengthTarget]}
|
||||||
|
DO: DoExpr {setvar[augInnateStrengthTarget, 0]}
|
||||||
|
IF: Not Expr {testvar[augInnateEnduranceTarget]}
|
||||||
|
DO: DoExpr {setvar[augInnateEnduranceTarget, 0]}
|
||||||
|
IF: Not Expr {testvar[augInnateCoordinationTarget]}
|
||||||
|
DO: DoExpr {setvar[augInnateCoordinationTarget, 0]}
|
||||||
|
IF: Not Expr {testvar[augInnateQuicknessTarget]}
|
||||||
|
DO: DoExpr {setvar[augInnateQuicknessTarget, 0]}
|
||||||
|
IF: Not Expr {testvar[augInnateFocusTarget]}
|
||||||
|
DO: DoExpr {setvar[augInnateFocusTarget, 0]}
|
||||||
|
IF: Not Expr {testvar[augInnateSelfTarget]}
|
||||||
|
DO: DoExpr {setvar[augInnateSelfTarget, 0]}
|
||||||
|
IF: Not Expr {testvar[augResistBluntTarget]}
|
||||||
|
DO: DoExpr {setvar[augResistBluntTarget, 0]}
|
||||||
|
IF: Not Expr {testvar[augResistSlashTarget]}
|
||||||
|
DO: DoExpr {setvar[augResistSlashTarget, 0]}
|
||||||
|
IF: Not Expr {testvar[augResistPierceTarget]}
|
||||||
|
DO: DoExpr {setvar[augResistPierceTarget, 0]}
|
||||||
|
IF: Not Expr {testvar[augResistLightningTarget]}
|
||||||
|
DO: DoExpr {setvar[augResistLightningTarget, 0]}
|
||||||
|
IF: Not Expr {testvar[augResistFireTarget]}
|
||||||
|
DO: DoExpr {setvar[augResistFireTarget, 0]}
|
||||||
|
IF: Not Expr {testvar[augResistFrostTarget]}
|
||||||
|
DO: DoExpr {setvar[augResistFrostTarget, 0]}
|
||||||
|
IF: Not Expr {testvar[augResistAcidTarget]}
|
||||||
|
DO: DoExpr {setvar[augResistAcidTarget, 0]}
|
||||||
|
IF: Not Expr {testvar[disableAugmentServices]}
|
||||||
|
DO: DoExpr {setvar[disableAugmentServices, 0]}
|
||||||
|
IF: Not Expr {testvar[serviceCheckInterval]}
|
||||||
|
DO: DoExpr {setvar[serviceCheckInterval, 43200]}
|
||||||
|
IF: Not Expr {testvar[serviceClock]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[serviceClock,stopwatchcreate[]]}
|
||||||
|
DoExpr {stopwatchstart[getvar[serviceClock]]}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ub opt set VTank.PatchExpressionEngine true}
|
||||||
|
Chat {/vt opt set autocram true}
|
||||||
|
ChatExpr {\/vt nav load bore_circuit+getvar[navNumber]}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
Chat {/vt opt set buffprofile_prots 2}
|
||||||
|
Chat {/vt opt set buffprofile_banes 5}
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
Chat {/vt opt set navpriorityboost false}
|
||||||
|
Chat {/vt opt set lootpriorityboost true}
|
||||||
|
Chat {/vt opt set lootonlyrarecorpses true}
|
||||||
|
DoExpr {setvar[lootclock,stopwatchcreate[]]}
|
||||||
|
DoExpr {stopwatchstart[getvar[lootclock]]}
|
||||||
|
SetState {service_quest_refresh}
|
||||||
|
~~ }
|
||||||
|
STATE: {service_quest_refresh} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/myquests}
|
||||||
|
DoExpr {touchvar[questsRequested]}
|
||||||
|
IF: All
|
||||||
|
Expr {testvar[questsRequested]}
|
||||||
|
Expr {isrefreshingquests[]==0}
|
||||||
|
SecsInStateGE 2
|
||||||
|
DO: SetState {service_pending_eval}
|
||||||
|
IF: SecsInStateGE 8
|
||||||
|
DO: SetState {service_pending_eval}
|
||||||
|
~~ }
|
||||||
|
STATE: {service_pending_eval} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[pendingAugments,0]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[230]<5,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[229]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[297]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[296]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[295]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[294]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[328]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[231]<3,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[232]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[234]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[237]<2,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[238]<5,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getvar[augInnateStrengthTarget]>0,iif[getcharintprop[217]<10,iif[getcharintprop[218]<getvar[augInnateStrengthTarget],1,getvar[pendingAugments]],getvar[pendingAugments]],getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getvar[augInnateEnduranceTarget]>0,iif[getcharintprop[217]<10,iif[getcharintprop[219]<getvar[augInnateEnduranceTarget],1,getvar[pendingAugments]],getvar[pendingAugments]],getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getvar[augInnateCoordinationTarget]>0,iif[getcharintprop[217]<10,iif[getcharintprop[220]<getvar[augInnateCoordinationTarget],1,getvar[pendingAugments]],getvar[pendingAugments]],getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getvar[augInnateQuicknessTarget]>0,iif[getcharintprop[217]<10,iif[getcharintprop[221]<getvar[augInnateQuicknessTarget],1,getvar[pendingAugments]],getvar[pendingAugments]],getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getvar[augInnateFocusTarget]>0,iif[getcharintprop[217]<10,iif[getcharintprop[222]<getvar[augInnateFocusTarget],1,getvar[pendingAugments]],getvar[pendingAugments]],getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getvar[augInnateSelfTarget]>0,iif[getcharintprop[217]<10,iif[getcharintprop[223]<getvar[augInnateSelfTarget],1,getvar[pendingAugments]],getvar[pendingAugments]],getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getvar[augResistBluntTarget]>0,iif[getcharintprop[239]<2,iif[getcharintprop[242]<getvar[augResistBluntTarget],1,getvar[pendingAugments]],getvar[pendingAugments]],getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getvar[augResistSlashTarget]>0,iif[getcharintprop[239]<2,iif[getcharintprop[240]<getvar[augResistSlashTarget],1,getvar[pendingAugments]],getvar[pendingAugments]],getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getvar[augResistPierceTarget]>0,iif[getcharintprop[239]<2,iif[getcharintprop[241]<getvar[augResistPierceTarget],1,getvar[pendingAugments]],getvar[pendingAugments]],getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getvar[augResistLightningTarget]>0,iif[getcharintprop[239]<2,iif[getcharintprop[246]<getvar[augResistLightningTarget],1,getvar[pendingAugments]],getvar[pendingAugments]],getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getvar[augResistFireTarget]>0,iif[getcharintprop[239]<2,iif[getcharintprop[244]<getvar[augResistFireTarget],1,getvar[pendingAugments]],getvar[pendingAugments]],getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getvar[augResistFrostTarget]>0,iif[getcharintprop[239]<2,iif[getcharintprop[245]<getvar[augResistFrostTarget],1,getvar[pendingAugments]],getvar[pendingAugments]],getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getvar[augResistAcidTarget]>0,iif[getcharintprop[239]<2,iif[getcharintprop[243]<getvar[augResistAcidTarget],1,getvar[pendingAugments]],getvar[pendingAugments]],getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[233]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[309]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[310]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[298]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[299]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[235]<4,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[236]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[226]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[225]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[227]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[228]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[224]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[300]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[302]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[301]<1,1,getvar[pendingAugments]]]}
|
||||||
|
DoExpr {setvar[pendingAugments, iif[getcharintprop[326]<1,1,getvar[pendingAugments]]]}
|
||||||
|
SetState {service_decide}
|
||||||
|
~~ }
|
||||||
|
STATE: {service_decide} ~~ {
|
||||||
|
IF: All
|
||||||
|
Expr {getqueststatus[`stipendtimer_0812`]==1}
|
||||||
|
DO: SetState {service_stipend}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[disableAugmentServices]!=1}
|
||||||
|
ItemCountGE 1 {Blank Augmentation Gem}
|
||||||
|
Expr {getvar[pendingAugments]==1}
|
||||||
|
DO: SetState {service_apply_augment}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[disableAugmentServices]!=1}
|
||||||
|
Expr {getqueststatus[`augmentationblankgemacquired`]==1}
|
||||||
|
Expr {getvar[pendingAugments]==1}
|
||||||
|
DO: SetState {service_bella_start}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[serviceClock,stopwatchcreate[]]}
|
||||||
|
DoExpr {stopwatchstart[getvar[serviceClock]]}
|
||||||
|
SetState {hunt}
|
||||||
|
~~ }
|
||||||
|
STATE: {service_stipend} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Always
|
||||||
|
DO: SetWatchdog 3 600 {service_reset_main}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
DoExpr {touchvar[stipendNavLoaded]}
|
||||||
|
EmbedNav nav0__stipend_nav {stipend.nav}
|
||||||
|
IF: All
|
||||||
|
Expr {testvar[stipendNavLoaded]}
|
||||||
|
NavEmpty
|
||||||
|
SecsInStateGE 2
|
||||||
|
DO: SetState {service_reset_main}
|
||||||
|
~~ }
|
||||||
|
STATE: {service_bella_start} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: SetState {service_bella_mp_primary}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/mp}
|
||||||
|
IF: SecsInStateGE 120
|
||||||
|
DO: SetState {service_reset_main}
|
||||||
|
~~ }
|
||||||
|
STATE: {service_bella_mp_primary} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load mp_primary}
|
||||||
|
Chat {/vt opt set enablebuffing true}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
Chat {/vt opt set NavPriorityBoost false}
|
||||||
|
Chat {/vt opt set navclosestoprange 0.008}
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load jaw_1}
|
||||||
|
SetState {service_bella_jaw_run}
|
||||||
|
IF: SecsInStateGE 300
|
||||||
|
DO: SetState {service_reset_main}
|
||||||
|
~~ }
|
||||||
|
STATE: {service_bella_jaw_run} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load jaw_1_hunt}
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
Chat {/vt opt set enablelooting true}
|
||||||
|
Chat {/vt opt set lootonlyrarecorpses false}
|
||||||
|
SetState {service_bella_jaw_hunt}
|
||||||
|
IF: SecsInStateGE 300
|
||||||
|
DO: SetState {service_reset_main}
|
||||||
|
~~ }
|
||||||
|
STATE: {service_bella_jaw_hunt} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
SetState {service_bella_turn_in_jaw}
|
||||||
|
IF: ItemCountGE 1 {Insatiable Eater Jaw}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load to_fiun}
|
||||||
|
IF: SecsInStateGE 1200
|
||||||
|
DO: SetState {service_reset_main}
|
||||||
|
~~ }
|
||||||
|
STATE: {service_bella_turn_in_jaw} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: ChatMatch {^Fiun Rehlyun.*}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/mp}
|
||||||
|
SetState {service_bella_mp_trans}
|
||||||
|
IF: SecsInStateGE 300
|
||||||
|
DO: SetState {service_reset_main}
|
||||||
|
~~ }
|
||||||
|
STATE: {service_bella_mp_trans} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load mp_secondary}
|
||||||
|
SetState {service_bella_mp_secondary}
|
||||||
|
IF: SecsInStateGE 120
|
||||||
|
DO: SetState {service_reset_main}
|
||||||
|
~~ }
|
||||||
|
STATE: {service_bella_mp_secondary} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
Chat {/vt nav load to_bella}
|
||||||
|
SetState {service_bella_kill_bella}
|
||||||
|
IF: SecsInStateGE 300
|
||||||
|
DO: SetState {service_reset_main}
|
||||||
|
~~ }
|
||||||
|
STATE: {service_bella_kill_bella} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: ItemCountGE 1 {Blank Augmentation Gem}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
SetState {service_bella_escape}
|
||||||
|
IF: SecsInStateGE 300
|
||||||
|
DO: SetState {service_reset_main}
|
||||||
|
~~ }
|
||||||
|
STATE: {service_bella_escape} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
IF: Always
|
||||||
|
DO: SetWatchdog 3 120 {service_reset_main}
|
||||||
|
IF: SecsInStateGE 1
|
||||||
|
DO: ChatExpr {\/ub setmotion forward 1}
|
||||||
|
IF: SecsInStateGE 4
|
||||||
|
DO: ChatExpr {\/ub setmotion forward 0}
|
||||||
|
IF: SecsInStateGE 6
|
||||||
|
DO: Chat {/vt opt set enablecombat false}
|
||||||
|
IF: SecsInStateGE 7
|
||||||
|
DO: Chat {/ah}
|
||||||
|
IF: SecsInStateGE 17
|
||||||
|
DO: SetState {service_apply_augment}
|
||||||
|
~~ }
|
||||||
|
STATE: {service_apply_augment} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: ItemCountLE 0 {Blank Augmentation Gem}
|
||||||
|
DO: SetState {service_reset_main}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {clearvar[augmentHub]}
|
||||||
|
DoExpr {clearvar[augmentHubLoaded]}
|
||||||
|
DoExpr {clearvar[augmentTargetLoaded]}
|
||||||
|
DoExpr {setvar[augmentStep,0]}
|
||||||
|
IF: Expr {getcharintprop[230]<5}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_husoon`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Might of the Seventh Mule`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[229]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_dumidabintruminre`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Shadow of the Seventh Mule`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[297]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_rapheldetante`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Infused War Magic`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[296]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_akemifei`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Infused Life Magic`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[295]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_ganfo`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Infused Item Magic`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[294]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_gustuvlansdown`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Infused Creature Magic`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[328]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_morathe`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Infused Void Magic`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[231]<3}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_rohulabintludun`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Clutch of the Miser`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[232]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_erikfestus`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Enduring Enchantment`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[234]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_rickarddumalia`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Quick Learner`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[237]<2}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_alisondulane`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Innate Renewal`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[238]<5}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_nawamaraujio`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Archmage's Endurance`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augInnateStrengthTarget]>0}
|
||||||
|
Expr {getcharintprop[217]<10}
|
||||||
|
Expr {getcharintprop[218]<getvar[augInnateStrengthTarget]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_fiunluunere`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Reinforcement of the Lugians`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augInnateEnduranceTarget]>0}
|
||||||
|
Expr {getcharintprop[217]<10}
|
||||||
|
Expr {getcharintprop[219]<getvar[augInnateEnduranceTarget]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_fiunruun`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Bleeargh's Fortitude`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augInnateCoordinationTarget]>0}
|
||||||
|
Expr {getcharintprop[217]<10}
|
||||||
|
Expr {getcharintprop[220]<getvar[augInnateCoordinationTarget]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_fiunbayaas`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Oswald's Enhancement`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augInnateQuicknessTarget]>0}
|
||||||
|
Expr {getcharintprop[217]<10}
|
||||||
|
Expr {getcharintprop[221]<getvar[augInnateQuicknessTarget]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_fiunriish`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Siraluun's Blessing`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augInnateFocusTarget]>0}
|
||||||
|
Expr {getcharintprop[217]<10}
|
||||||
|
Expr {getcharintprop[222]<getvar[augInnateFocusTarget]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_fiunvasherr`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Enduring Calm`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augInnateSelfTarget]>0}
|
||||||
|
Expr {getcharintprop[217]<10}
|
||||||
|
Expr {getcharintprop[223]<getvar[augInnateSelfTarget]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_fiunnoress`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Steadfast Will`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augResistBluntTarget]>0}
|
||||||
|
Expr {getcharintprop[239]<2}
|
||||||
|
Expr {getcharintprop[242]<getvar[augResistBluntTarget]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_nawamaradia`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Enhancement of the Mace Turner`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augResistSlashTarget]>0}
|
||||||
|
Expr {getcharintprop[239]<2}
|
||||||
|
Expr {getcharintprop[240]<getvar[augResistSlashTarget]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_ilinwis`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Enhancement of the Blade Turner`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augResistPierceTarget]>0}
|
||||||
|
Expr {getcharintprop[239]<2}
|
||||||
|
Expr {getcharintprop[241]<getvar[augResistPierceTarget]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_kyujorujen`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Enhancement of the Arrow Turner`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augResistLightningTarget]>0}
|
||||||
|
Expr {getcharintprop[239]<2}
|
||||||
|
Expr {getcharintprop[246]<getvar[augResistLightningTarget]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_enliyuo`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Storm's Enhancement`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augResistFireTarget]>0}
|
||||||
|
Expr {getcharintprop[239]<2}
|
||||||
|
Expr {getcharintprop[244]<getvar[augResistFireTarget]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_rikshenri`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Fiery Enhancement`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augResistFrostTarget]>0}
|
||||||
|
Expr {getcharintprop[239]<2}
|
||||||
|
Expr {getcharintprop[245]<getvar[augResistFrostTarget]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_lubao`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Icy Enhancement`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augResistAcidTarget]>0}
|
||||||
|
Expr {getcharintprop[239]<2}
|
||||||
|
Expr {getcharintprop[243]<getvar[augResistAcidTarget]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_shujiomilao`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Caustic Enhancement`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[233]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_piersantilinante`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Critical Protection`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[309]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_neelanashua`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Frenzy of the Slayer`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[310]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_emilyyarow`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Iron Skin of the Invincible`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[298]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_anframmellow`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Eye of the Remorseless`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[299]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_alishiabintaldan`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Hand of the Remorseless`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[235]<4}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_kriscennis`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Ciandra's Fortune`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[236]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_lug`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Charmed Smith`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[226]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_joshunfelden`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Jibril's Essence`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[225]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_briennecarlus`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Yoshi's Essence`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[227]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_burrellsammrun`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Celdiseth's Essence`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[228]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_lenorturk`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Koga's Essence`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[224]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_robertcrow`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Ciandra's Essence`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[300]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_carlitogallo`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Master of the Steel Circle`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[302]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_rahinabintzalanis`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Master of the Five Fold Path`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[301]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_kilaf`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Master of the Focused Eye`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Expr {getcharintprop[326]<1}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentNav,`nav_ariannatheadept`]}
|
||||||
|
DoExpr {setvar[augmentItem,`Jack of All Trades`]}
|
||||||
|
SetState {service_apply_augment_nav}
|
||||||
|
IF: Always
|
||||||
|
DO: SetState {service_reset_main}
|
||||||
|
~~ }
|
||||||
|
STATE: {service_apply_augment_nav} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Not Expr {testvar[augmentStep]}
|
||||||
|
DO: DoExpr {setvar[augmentStep,0]}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augmentStep]==0}
|
||||||
|
Any
|
||||||
|
Expr {getvar[augmentNav]==`nav_husoon`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_dumidabintruminre`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_rahinabintzalanis`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_kilaf`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentHub,`nav_zaikhal`]}
|
||||||
|
DoExpr {setvar[augmentStep,1]}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augmentStep]==0}
|
||||||
|
Any
|
||||||
|
Expr {getvar[augmentNav]==`nav_rapheldetante`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_rickarddumalia`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_carlitogallo`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentHub,`nav_silyun`]}
|
||||||
|
DoExpr {setvar[augmentStep,1]}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augmentStep]==0}
|
||||||
|
Any
|
||||||
|
Expr {getvar[augmentNav]==`nav_akemifei`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_ganfo`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_nawamaradia`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_ilinwis`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_kyujorujen`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_enliyuo`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_rikshenri`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_lubao`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_shujiomilao`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentHub,`nav_hebianto`]}
|
||||||
|
DoExpr {setvar[augmentStep,1]}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augmentStep]==0}
|
||||||
|
Any
|
||||||
|
Expr {getvar[augmentNav]==`nav_gustuvlansdown`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_emilyyarow`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_kriscennis`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_joshunfelden`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_briennecarlus`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_burrellsammrun`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_lenorturk`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_robertcrow`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentHub,`nav_cragstone`]}
|
||||||
|
DoExpr {setvar[augmentStep,1]}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augmentStep]==0}
|
||||||
|
Any
|
||||||
|
Expr {getvar[augmentNav]==`nav_rohulabintludun`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_erikfestus`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_anframmellow`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_alishiabintaldan`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentHub,`nav_ab`]}
|
||||||
|
DoExpr {setvar[augmentStep,1]}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augmentStep]==0}
|
||||||
|
Any
|
||||||
|
Expr {getvar[augmentNav]==`nav_alisondulane`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_neelanashua`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_ariannatheadept`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentHub,`nav_bandit`]}
|
||||||
|
DoExpr {setvar[augmentStep,1]}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augmentStep]==0}
|
||||||
|
Any
|
||||||
|
Expr {getvar[augmentNav]==`nav_fiunluunere`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_fiunruun`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_fiunbayaas`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_fiunriish`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_fiunvasherr`}
|
||||||
|
Expr {getvar[augmentNav]==`nav_fiunnoress`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentHub,`nav_fiun`]}
|
||||||
|
DoExpr {setvar[augmentStep,1]}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augmentStep]==0}
|
||||||
|
Expr {getvar[augmentNav]==`nav_piersantilinante`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentHub,`nav_sanamar`]}
|
||||||
|
DoExpr {setvar[augmentStep,1]}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augmentStep]==0}
|
||||||
|
Expr {getvar[augmentNav]==`nav_lug`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentHub,`nav_oolutangasrefuge`]}
|
||||||
|
DoExpr {setvar[augmentStep,1]}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augmentStep]==0}
|
||||||
|
Expr {getvar[augmentNav]==`nav_nawamaraujio`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentHub,`nav_mayoi`]}
|
||||||
|
DoExpr {setvar[augmentStep,1]}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augmentStep]==0}
|
||||||
|
Expr {getvar[augmentNav]==`nav_morathe`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[augmentHub,`nav_candeth`]}
|
||||||
|
DoExpr {setvar[augmentStep,1]}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augmentStep]==1}
|
||||||
|
Expr {testvar[augmentHub]}
|
||||||
|
Not Expr {testvar[augmentHubLoaded]}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
Chat {/vt opt set enablebuffing true}
|
||||||
|
DoExpr {touchvar[augmentHubLoaded]}
|
||||||
|
ChatExpr {\/vt nav load +getvar[augmentHub]}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augmentStep]==1}
|
||||||
|
Expr {testvar[augmentHubLoaded]}
|
||||||
|
NavEmpty
|
||||||
|
SecsInStateGE 2
|
||||||
|
DO: DoExpr {setvar[augmentStep,2]}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augmentStep]==2}
|
||||||
|
Not Expr {testvar[augmentTargetLoaded]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {touchvar[augmentTargetLoaded]}
|
||||||
|
ChatExpr {\/vt nav load +getvar[augmentNav]}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
DoExpr {setvar[augmentStep,3]}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[augmentStep]==3}
|
||||||
|
Expr {testvar[augmentTargetLoaded]}
|
||||||
|
NavEmpty
|
||||||
|
SecsInStateGE 2
|
||||||
|
DO: SetState {service_use_augment}
|
||||||
|
IF: Always
|
||||||
|
DO: SetWatchdog 3 600 {service_reset_main}
|
||||||
|
~~ }
|
||||||
|
STATE: {service_use_augment} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Not Expr {testvar[augmentItem]}
|
||||||
|
DO: SetState {service_reset_main}
|
||||||
|
IF: Always
|
||||||
|
DO: SetWatchdog 3 120 {service_reset_main}
|
||||||
|
IF: SecsInStateGE 3
|
||||||
|
DO: Chat {/ub prepclick yes 10}
|
||||||
|
IF: SecsInStateGE 7
|
||||||
|
DO: ChatExpr {\/ub use +getvar[augmentItem]}
|
||||||
|
IF: SecsInStateGE 15
|
||||||
|
DO: SetState {service_reset_main}
|
||||||
|
~~ }
|
||||||
|
STATE: {service_reset_main} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {clearvar[questsRequested]}
|
||||||
|
DoExpr {clearvar[stipendNavLoaded]}
|
||||||
|
DoExpr {clearvar[augmentNav]}
|
||||||
|
DoExpr {clearvar[augmentHub]}
|
||||||
|
DoExpr {clearvar[augmentHubLoaded]}
|
||||||
|
DoExpr {clearvar[augmentTargetLoaded]}
|
||||||
|
DoExpr {clearvar[augmentStep]}
|
||||||
|
DoExpr {clearvar[augmentItem]}
|
||||||
|
DoExpr {setvar[serviceClock,stopwatchcreate[]]}
|
||||||
|
DoExpr {stopwatchstart[getvar[serviceClock]]}
|
||||||
|
SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {avoid_portal} ~~ {
|
||||||
|
IF: SecsInStateGE 1
|
||||||
|
DO: ChatExpr {\/me portalHeading \= +getvar[portalHeading]}
|
||||||
|
IF: SecsInStateGE 2
|
||||||
|
DO: ChatExpr {\/me playerHeading \= +getvar[playerHeading]}
|
||||||
|
IF: SecsInStateGE 3
|
||||||
|
DO: ChatExpr {\/me relativeAngle \= +getvar[relativeAngle]}
|
||||||
|
IF: SecsInStateGE 4
|
||||||
|
DO: ChatExpr {\/me Portal detected ahead\! Avoiding now\!}
|
||||||
|
IF: SecsInStateGE 5
|
||||||
|
DO: ChatExpr {\/me is too close to +wobjectgetname[wobjectfindnearestbyobjectclass[14]]}
|
||||||
|
IF: SecsInStateGE 6
|
||||||
|
DO: ChatExpr {\/me at +coordinatetostring[getplayercoordinates[]]}
|
||||||
|
IF: SecsInStateGE 7
|
||||||
|
DO: ChatExpr {\/ub face +getvar[newHeading]}
|
||||||
|
IF: SecsInStateGE 9
|
||||||
|
DO: ChatExpr {\/ub setmotion forward 1}
|
||||||
|
IF: SecsInStateGE 11
|
||||||
|
DO: ChatExpr {\/ub setmotion forward 0}
|
||||||
|
IF: SecsInStateGE 12
|
||||||
|
DO: ChatExpr {\/ub face +getvar[playerHeading]}
|
||||||
|
IF: SecsInStateGE 14
|
||||||
|
DO: ChatExpr {\/ub setmotion forward 1}
|
||||||
|
IF: SecsInStateGE 16
|
||||||
|
DO: ChatExpr {\/ub setmotion forward 0}
|
||||||
|
IF: SecsInStateGE 17
|
||||||
|
DO: ChatExpr {\/mm nextwp}
|
||||||
|
IF: SecsInStateGE 18
|
||||||
|
DO: ChatExpr {\/vt opt set enablenav true}
|
||||||
|
IF: SecsInStateGE 19
|
||||||
|
DO: ChatExpr {\/vt setmetastate hunt}
|
||||||
|
~~ }
|
||||||
|
STATE: {navbore} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt opt set lootonlyrarecorpses true}
|
||||||
|
IF: Always
|
||||||
|
DO: ChatExpr {\/vt nav load bore_route+getvar[navNumber]}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt opt set enablebuffing true}
|
||||||
|
IF: Always
|
||||||
|
DO: SetWatchdog 3 300 {navbore}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoAll
|
||||||
|
ChatExpr {\/vt nav load bore_circuit+getvar[navNumber]}
|
||||||
|
SetState {hunt}
|
||||||
|
~~ }
|
||||||
|
STATE: {recomp} ~~ {
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {navbore}
|
||||||
|
IF: Always
|
||||||
|
DO: SetWatchdog 3 300 {navbore}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt opt set enablebuffing false}
|
||||||
|
Chat {/vt nav load ah_rarecomp}
|
||||||
|
~~ }
|
||||||
|
STATE: {ring_fail} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
SetWatchdog 100 3 {hunt}
|
||||||
|
~~ }
|
||||||
|
STATE: {loot} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablelooting true}
|
||||||
|
Chat {/vt opt set lootonlyrarecorpses false}
|
||||||
|
Chat {/vt opt set lootpriorityboost false}
|
||||||
|
IF: SecsInStateGE 300
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set lootonlyrarecorpses true}
|
||||||
|
Chat {/vt opt set lootpriorityboost true}
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
DoExpr {setvar[lootclock,stopwatchcreate[]]}
|
||||||
|
DoExpr {stopwatchstart[getvar[lootclock]]}
|
||||||
|
SetState {hunt}
|
||||||
|
IF: All
|
||||||
|
Expr {setvar[portal, wobjectfindnearestbyobjectclass[14]]}
|
||||||
|
Expr {istrue[getvar[portal]]}
|
||||||
|
Expr {coordinatedistancewithz[getplayercoordinates[], wobjectgetphysicscoordinates[getvar[portal]]] <= 8}
|
||||||
|
Not Expr {wobjectgetname[getvar[portal]]==Singularity Caul}
|
||||||
|
Not Expr {wobjectgetname[getvar[portal]]#Destroyed.*}
|
||||||
|
Expr {wobjectisvalid[getvar[portal]]}
|
||||||
|
Expr {setvar[portalHeading, getheadingto[getvar[portal]]]}
|
||||||
|
Expr {setvar[playerHeading, getheading[wobjectgetplayer[]]]}
|
||||||
|
Expr {setvar[relativeAngle, abs[getvar[portalHeading] - getvar[playerHeading]]]}
|
||||||
|
Expr {setvar[relativeAngle, iif[getvar[relativeAngle] > 180, 360 - getvar[relativeAngle], getvar[relativeAngle]]]}
|
||||||
|
Expr {setvar[turnDirection, iif[randint[0,2] == 0, -90, 90]]}
|
||||||
|
Expr {getvar[relativeAngle] <= 11.25}
|
||||||
|
Expr {setvar[newHeading, abs[getvar[playerHeading] + getvar[turnDirection]]]}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablenav false}
|
||||||
|
Chat {/vt opt set lootonlyrarecorpses true}
|
||||||
|
Chat {/vt opt set lootpriorityboost true}
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
DoExpr {setvar[lootclock,stopwatchcreate[]]}
|
||||||
|
DoExpr {stopwatchstart[getvar[lootclock]]}
|
||||||
|
SetState {avoid_portal}
|
||||||
|
~~ }
|
||||||
|
|
||||||
|
~~========================= ONLY NAVS APPEAR BELOW THIS LINE =========================~~
|
||||||
|
|
||||||
|
NAV: nav0__stipend_nav once ~~ {
|
||||||
|
cht -101.597905190786 -96.6216093699137 2.08333134651184E-05 {/ah}
|
||||||
|
pnt 59.3590666453044 -28.7057823816935 0.0500208298365275
|
||||||
|
pnt 59.3771680514018 -28.7152192115784 0.0500208298365275
|
||||||
|
ptl -101.597905190786 -96.6216093699137 2.08333134651184E-05 59.3936458587647 -28.7256083488464 0.0508250035345554 14 {Portal to Town Network}
|
||||||
|
pnt -101.615851815542 -96.6388638178507 2.08333134651184E-05
|
||||||
|
pnt -101.657751337687 -96.5832635879517 2.08333134651184E-05
|
||||||
|
pnt -101.658352184296 -96.5325949986776 2.08333134651184E-05
|
||||||
|
pnt -101.605949529012 -96.519695186615 2.08333134651184E-05
|
||||||
|
ptl -101.597905190786 -96.6216093699137 2.08333134651184E-05 -101.588099161784 -96.5166525046031 -0.000262499845121056 14 {Portal to Arwic}
|
||||||
|
pnt 56.6498762130737 33.416518386205 0.175020837783813
|
||||||
|
pnt 56.655900033315 33.5368880271912 0.175020837783813
|
||||||
|
pnt 56.7470087051392 33.5495386441549 0.175020837783813
|
||||||
|
pnt 56.7795230229696 33.6337207794189 0.175020837783813
|
||||||
|
tlk -101.597905190786 -96.6216093699137 2.08333134651184E-05 56.7816291809082 33.6455291748047 0.175020843744278 37 {Monroe}
|
||||||
|
~~ }
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
~~
|
||||||
|
~~ Bore Dungeon Quest Meta
|
||||||
|
~~ Navigates to the Bore dungeon, fights through to the boss,
|
||||||
|
~~ loots the quest item, returns to town to turn in.
|
||||||
|
~~ Includes death recovery.
|
||||||
|
~~
|
||||||
|
|
||||||
|
STATE: {Default}
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {Death}
|
||||||
|
IF: ItemCountGE 1 {Bore Trophy}
|
||||||
|
DO: SetState {TurnIn}
|
||||||
|
IF: ItemCountLE 50 {Prismatic Taper}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {Running low on tapers, restocking.}
|
||||||
|
SetState {Restock}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
EmbedNav navToBore {Navigate to Bore dungeon entrance}
|
||||||
|
SetWatchdog 10.0 180.0 {Stuck}
|
||||||
|
|
||||||
|
STATE: {EnterDungeon}
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {Death}
|
||||||
|
IF: IntoPortal
|
||||||
|
DO: SetState {InsideDungeon}
|
||||||
|
IF: SecsInStateGE 30
|
||||||
|
DO: SetState {Default}
|
||||||
|
|
||||||
|
STATE: {InsideDungeon}
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {Death}
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: DoAll
|
||||||
|
SetOpt {enablecombat} {true}
|
||||||
|
SetOpt {enablelooting} {true}
|
||||||
|
EmbedNav navDungeonRoute {Run through dungeon to boss}
|
||||||
|
SetWatchdog 10.0 120.0 {Stuck}
|
||||||
|
|
||||||
|
STATE: {BossRoom}
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {Death}
|
||||||
|
IF: MobsInDist_Name 1 30.0 {Bore Boss}
|
||||||
|
DO: DoAll
|
||||||
|
SetOpt {enablecombat} {true}
|
||||||
|
SetState {Fighting}
|
||||||
|
IF: All
|
||||||
|
NavEmpty
|
||||||
|
NoMobsInDist 25.0
|
||||||
|
DO: SetState {LootBoss}
|
||||||
|
IF: Always
|
||||||
|
DO: None
|
||||||
|
|
||||||
|
STATE: {Fighting}
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {Death}
|
||||||
|
IF: NoMobsInDist 25.0
|
||||||
|
DO: SetState {LootBoss}
|
||||||
|
IF: SecsInStateGE 300
|
||||||
|
DO: SetState {BossRoom}
|
||||||
|
|
||||||
|
STATE: {LootBoss}
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {Death}
|
||||||
|
IF: ItemCountGE 1 {Bore Trophy}
|
||||||
|
DO: SetState {ExitDungeon}
|
||||||
|
IF: SecsInStateGE 30
|
||||||
|
DO: DoAll
|
||||||
|
Chat {No trophy found, exiting anyway.}
|
||||||
|
SetState {ExitDungeon}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
SetOpt {enablelooting} {true}
|
||||||
|
None
|
||||||
|
|
||||||
|
STATE: {ExitDungeon}
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {Death}
|
||||||
|
IF: All
|
||||||
|
NavEmpty
|
||||||
|
DO: DoAll
|
||||||
|
SetOpt {enablecombat} {false}
|
||||||
|
SetOpt {enablelooting} {false}
|
||||||
|
SetState {TurnIn}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
EmbedNav navExitDungeon {Exit the dungeon}
|
||||||
|
SetWatchdog 10.0 120.0 {Stuck}
|
||||||
|
|
||||||
|
STATE: {TurnIn}
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {Death}
|
||||||
|
IF: All
|
||||||
|
NavEmpty
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ub givep Bore Trophy to Bore Quest NPC}
|
||||||
|
Chat {/ub prepclick yes 3}
|
||||||
|
SetState {WaitTurnIn}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
EmbedNav navToNPC {Go to turn-in NPC}
|
||||||
|
SetWatchdog 10.0 120.0 {Stuck}
|
||||||
|
|
||||||
|
STATE: {WaitTurnIn}
|
||||||
|
IF: ChatMatch {^Bore Quest NPC tells you,}
|
||||||
|
DO: SetState {TurnInComplete}
|
||||||
|
IF: ChatCapture {You have solved this quest too recently!} {}
|
||||||
|
DO: SetState {QuestCooldown}
|
||||||
|
IF: SecsInStateGE 15
|
||||||
|
DO: SetState {TurnIn}
|
||||||
|
|
||||||
|
STATE: {TurnInComplete}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {Quest turn-in complete! Restarting.}
|
||||||
|
SetState {Default}
|
||||||
|
|
||||||
|
STATE: {QuestCooldown}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {Quest on cooldown, waiting...}
|
||||||
|
SetState {Default}
|
||||||
|
|
||||||
|
STATE: {Restock}
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {Death}
|
||||||
|
IF: All
|
||||||
|
NavEmpty
|
||||||
|
VendorOpen
|
||||||
|
DO: SetState {Selling}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
EmbedNav navRestock {Go to vendor to restock}
|
||||||
|
SetWatchdog 10.0 120.0 {Stuck}
|
||||||
|
|
||||||
|
STATE: {Selling}
|
||||||
|
IF: VendorClosed
|
||||||
|
DO: SetState {Default}
|
||||||
|
IF: SecsInStateGE 30
|
||||||
|
DO: SetState {Default}
|
||||||
|
|
||||||
|
STATE: {Stuck}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ub face}
|
||||||
|
SetState {Default}
|
||||||
|
|
||||||
|
STATE: {Death}
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: SetState {Default}
|
||||||
|
|
||||||
|
~~ ============================================================
|
||||||
|
~~ NAV ROUTES
|
||||||
|
~~ ============================================================
|
||||||
|
|
||||||
|
NAV: navToBore once
|
||||||
|
~~ Route from lifestone to Bore dungeon entrance
|
||||||
|
rcl 0.0 0.0 0.0 {Lifestone Recall}
|
||||||
|
pnt 0.0 0.0 0.0
|
||||||
|
pnt 0.0 0.0 0.0
|
||||||
|
ptl 0.0 0.0 0.0 0.0 0.0 0.0 14 {Bore Dungeon}
|
||||||
|
|
||||||
|
NAV: navDungeonRoute once
|
||||||
|
~~ Route through dungeon to boss room
|
||||||
|
pnt 0.0 0.0 0.0
|
||||||
|
pnt 0.0 0.0 0.0
|
||||||
|
pnt 0.0 0.0 0.0
|
||||||
|
pnt 0.0 0.0 0.0
|
||||||
|
|
||||||
|
NAV: navExitDungeon once
|
||||||
|
~~ Route from boss room back to exit portal
|
||||||
|
pnt 0.0 0.0 0.0
|
||||||
|
pnt 0.0 0.0 0.0
|
||||||
|
ptl 0.0 0.0 0.0 0.0 0.0 0.0 14 {Exit Portal}
|
||||||
|
|
||||||
|
NAV: navToNPC once
|
||||||
|
~~ Route to quest turn-in NPC
|
||||||
|
rcl 0.0 0.0 0.0 {Lifestone Recall}
|
||||||
|
pnt 0.0 0.0 0.0
|
||||||
|
tlk 0.0 0.0 0.0 0.0 0.0 0.0 37 {Bore Quest NPC}
|
||||||
|
|
||||||
|
NAV: navRestock once
|
||||||
|
~~ Route to vendor for restocking tapers
|
||||||
|
rcl 0.0 0.0 0.0 {Lifestone Recall}
|
||||||
|
pnt 0.0 0.0 0.0
|
||||||
|
vnd 0.0 0.0 0.0 00000000 {Vendor}
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
~~ {
|
||||||
|
~~ FOR AUTO-COMPLETION ASSISTANCE: testvar getvar setvar touchvar clearallvars clearvar getcharintprop getchardoubleprop getcharquadprop getcharboolprop getcharstringprop getisspellknown getcancastspell_hunt getcancastspell_buff getcharvital_base getcharvital_current getcharvital_buffedmax getcharskill_traininglevel getcharskill_base getcharskill_buffed getplayerlandcell getplayercoordinates coordinategetns coordinategetwe coordinategetz coordinatetostring coordinateparse coordinatedistancewithz coordinatedistanceflat wobjectgetphysicscoordinates wobjectgetname wobjectgetobjectclass wobjectgettemplatetype wobjectgetisdooropen wobjectfindnearestmonster wobjectfindnearestdoor wobjectfindnearestbyobjectclass wobjectfindininventorybytemplatetype wobjectfindininventorybyname wobjectfindininventorybynamerx wobjectgetselection wobjectgetplayer wobjectfindnearestbynameandobjectclass actiontryselect actiontryuseitem actiontryapplyitem actiontrygiveitem actiontryequipanywand actiontrycastbyid actiontrycastbyidontarget chatbox chatboxpaste statushud statushudcolored uigetcontrol uisetlabel isfalse istrue iif randint cstr strlen getobjectinternaltype cstrf stopwatchcreate stopwatchstart stopwatchstop stopwatchelapsedseconds cnumber floor ceiling round abs getworldname getitemcountininventorybyname getheading getitemcountininventorybynamerx getheadingto actiontrygiveprofile vitae getfellowshipstatus getfellowshipname getfellowshipisopen getfellowshipisleader getfellowshipleaderid getfellowshipcanrecruit getfellowid getfellowshipcount getfellowshiplocked getfellowname getfellowshipisfull sin cos tan sqrt asin acos atan atan2 sinh cosh tanh vtsetmetastate getregexmatch echo chr ord wobjectgetid wobjectgethealth wobjectfindbyid wobjectgetintprop wobjectfindnearestbytemplatetype wobjectgetopencontainer testquestflag getquestktprogress isrefreshingquests getquestktrequired getqueststatus getisday getgamehour getgamehourname getisnight getgameday getgameticks getminutesuntilday getgamemonth getgamemonthname getminutesuntilnight getgameyear uisetvisible uiviewvisible uiviewexists getgvar touchgvar getpvar touchpvar setgvar cleargvar setpvar clearpvar testgvar clearallgvars testpvar clearallpvars dictgetitem dictcreate dicthaskey dictadditem dictkeys dictremovekey dictvalues dictclear dictsize dictcopy listgetitem listpop listcreate listcontains listremove listadd listindexof listremoveat listinsert listlastindexof listclear listcopy listcount listreverse
|
||||||
|
|
||||||
|
~~
|
||||||
|
~~ File auto-generated by metaf, a program created by Eskarina of Morningthaw/Coldeve.
|
||||||
|
~~ Get metaf here: https://github.com/JJEII/metaf/
|
||||||
|
~~
|
||||||
|
~~ All recognized structural designators:
|
||||||
|
~~ STATE: DO:
|
||||||
|
~~ IF: NAV:
|
||||||
|
~~
|
||||||
|
~~ All recognized CONDITION (IF:) operation keywords:
|
||||||
|
~~ Never NavEmpty MobsInDist_Priority Not
|
||||||
|
~~ Always Death NeedToBuff PSecsInStateGE
|
||||||
|
~~ All VendorOpen NoMobsInDist SecsOnSpellGE
|
||||||
|
~~ Any VendorClosed BlockE BuPercentGE
|
||||||
|
~~ ChatMatch ItemCountLE CellE DistToRteGE
|
||||||
|
~~ MainSlotsLE ItemCountGE IntoPortal Expr
|
||||||
|
~~ SecsInStateGE MobsInDist_Name ExitPortal ChatCapture
|
||||||
|
~~
|
||||||
|
~~ All recognized ACTION (DO:) operation keywords:
|
||||||
|
~~ None EmbedNav ChatExpr SetOpt
|
||||||
|
~~ SetState CallState SetWatchdog CreateView
|
||||||
|
~~ Chat Return ClearWatchdog DestroyView
|
||||||
|
~~ DoAll DoExpr GetOpt DestroyAllViews
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV types:
|
||||||
|
~~ circular follow
|
||||||
|
~~ linear once
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV NODE types:
|
||||||
|
~~ flw vnd
|
||||||
|
~~ pnt ptl
|
||||||
|
~~ rcl tlk
|
||||||
|
~~ pau chk
|
||||||
|
~~ cht jmp
|
||||||
|
~~ prt (deprecated in VTank)
|
||||||
|
~~ }
|
||||||
|
|
||||||
|
STATE: {Default} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ub opt set VTank.PatchExpressionEngine true}
|
||||||
|
SetOpt {enablecombat} {false}
|
||||||
|
SetOpt {enablenav} {true}
|
||||||
|
SetOpt {enablelooting} {false}
|
||||||
|
SetOpt {enablebuffing} {true}
|
||||||
|
SetOpt {attackdistance} {0.0125}
|
||||||
|
DoExpr {setvar[currentLevel,`upper`]}
|
||||||
|
DoExpr {setvar[currentRoom,`north`]}
|
||||||
|
DoExpr {setvar[roomsDone,0]}
|
||||||
|
Chat {/vt nav load nav_empyrean}
|
||||||
|
SetState {enter_dungeon}
|
||||||
|
~~ }
|
||||||
|
STATE: {enter_dungeon} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: All
|
||||||
|
SecsInStateGE 3
|
||||||
|
NavEmpty
|
||||||
|
DO: DoAll
|
||||||
|
SetOpt {enablecombat} {true}
|
||||||
|
SetState {goto_room}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
SetWatchdog 10 300 {stuck}
|
||||||
|
~~ }
|
||||||
|
STATE: {goto_room} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
ChatExpr {\/vt nav load emp_+getvar[currentLevel]+`_`+getvar[currentRoom]}
|
||||||
|
DoExpr {clearvar[navLoaded]}
|
||||||
|
SetState {arrive_room}
|
||||||
|
~~ }
|
||||||
|
STATE: {arrive_room} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: All
|
||||||
|
Expr {testvar[navLoaded]}
|
||||||
|
NavEmpty
|
||||||
|
DO: DoAll
|
||||||
|
ClearWatchdog
|
||||||
|
SetState {kill_wisps}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {touchvar[navLoaded]}
|
||||||
|
SetWatchdog 10 180 {stuck}
|
||||||
|
~~ }
|
||||||
|
STATE: {kill_wisps} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: All
|
||||||
|
SecsInStateGE 5
|
||||||
|
NoMobsInDist 4
|
||||||
|
DO: SetState {use_stone}
|
||||||
|
IF: SecsInStateGE 300
|
||||||
|
DO: SetState {use_stone}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
SetWatchdog 10 300 {stuck}
|
||||||
|
~~ }
|
||||||
|
STATE: {use_stone} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: ChatMatch {A shiver moves across your body}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {echo[`Room complete! (`+getvar[currentLevel]+` `+getvar[currentRoom]+`)`,1]}
|
||||||
|
SetState {next_room}
|
||||||
|
IF: ChatMatch {no change}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {echo[`Room already done (`+getvar[currentLevel]+` `+getvar[currentRoom]+`)`,1]}
|
||||||
|
SetState {next_room}
|
||||||
|
IF: SecsInStateGE 5
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ub use Aligned Mana Stone}
|
||||||
|
SetState {use_stone}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ub use Aligned Mana Stone}
|
||||||
|
~~ }
|
||||||
|
STATE: {next_room} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Expr {getvar[currentRoom]==`north`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[currentRoom,`east`]}
|
||||||
|
SetState {goto_room}
|
||||||
|
IF: Expr {getvar[currentRoom]==`east`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[currentRoom,`south`]}
|
||||||
|
SetState {goto_room}
|
||||||
|
IF: Expr {getvar[currentRoom]==`south`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[currentRoom,`west`]}
|
||||||
|
SetState {goto_room}
|
||||||
|
IF: Expr {getvar[currentRoom]==`west`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[currentRoom,`center`]}
|
||||||
|
SetState {goto_room}
|
||||||
|
IF: Expr {getvar[currentRoom]==`center`}
|
||||||
|
DO: SetState {use_stone_center}
|
||||||
|
~~ }
|
||||||
|
STATE: {use_stone_center} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: ChatMatch {A shiver moves across your body}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {echo[`Level complete! (`+getvar[currentLevel]+`)`,1]}
|
||||||
|
SetState {take_portal}
|
||||||
|
IF: ChatMatch {no change}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {echo[`Level already done (`+getvar[currentLevel]+`)`,1]}
|
||||||
|
SetState {take_portal}
|
||||||
|
IF: SecsInStateGE 5
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ub use Aligned Mana Stone}
|
||||||
|
SetState {use_stone_center}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ub use Aligned Mana Stone}
|
||||||
|
~~ }
|
||||||
|
STATE: {take_portal} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Expr {getvar[currentLevel]==`upper`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[currentLevel,`middle`]}
|
||||||
|
DoExpr {setvar[currentRoom,`north`]}
|
||||||
|
Chat {/ub use Empyrean Facility Middle Level}
|
||||||
|
SetState {portal_transition}
|
||||||
|
IF: Expr {getvar[currentLevel]==`middle`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[currentLevel,`lower`]}
|
||||||
|
DoExpr {setvar[currentRoom,`north`]}
|
||||||
|
Chat {/ub use Empyrean Facility Lower Level}
|
||||||
|
SetState {portal_transition}
|
||||||
|
IF: Expr {getvar[currentLevel]==`lower`}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {echo[`All 3 aetheria slots unlocked!`,1]}
|
||||||
|
SetState {quest_done}
|
||||||
|
~~ }
|
||||||
|
STATE: {portal_transition} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {echo[`Arrived at `+getvar[currentLevel]+` level`,1]}
|
||||||
|
SetState {goto_room}
|
||||||
|
IF: SecsInStateGE 15
|
||||||
|
DO: SetState {take_portal}
|
||||||
|
~~ }
|
||||||
|
STATE: {quest_done} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {echo[`Empyrean Facility quest COMPLETE! All aetheria slots unlocked.`,1]}
|
||||||
|
Chat {/mp}
|
||||||
|
~~ }
|
||||||
|
STATE: {stuck} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ub face}
|
||||||
|
DoExpr {echo[`Stuck! Trying to recover...`,1]}
|
||||||
|
SetState {goto_room}
|
||||||
|
~~ }
|
||||||
|
STATE: {death} ~~ {
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {echo[`Died! Re-entering dungeon...`,1]}
|
||||||
|
Chat {/vt nav load nav_empyrean}
|
||||||
|
SetState {enter_dungeon}
|
||||||
|
~~ }
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
~~ {
|
||||||
|
~~ FOR AUTO-COMPLETION ASSISTANCE: testvar getvar setvar touchvar clearallvars clearvar getcharintprop getchardoubleprop getcharquadprop getcharboolprop getcharstringprop getisspellknown getcancastspell_hunt getcancastspell_buff getcharvital_base getcharvital_current getcharvital_buffedmax getcharskill_traininglevel getcharskill_base getcharskill_buffed getplayerlandcell getplayercoordinates coordinategetns coordinategetwe coordinategetz coordinatetostring coordinateparse coordinatedistancewithz coordinatedistanceflat wobjectgetphysicscoordinates wobjectgetname wobjectgetobjectclass wobjectgettemplatetype wobjectgetisdooropen wobjectfindnearestmonster wobjectfindnearestdoor wobjectfindnearestbyobjectclass wobjectfindininventorybytemplatetype wobjectfindininventorybyname wobjectfindininventorybynamerx wobjectgetselection wobjectgetplayer wobjectfindnearestbynameandobjectclass actiontryselect actiontryuseitem actiontryapplyitem actiontrygiveitem actiontryequipanywand actiontrycastbyid actiontrycastbyidontarget chatbox chatboxpaste statushud statushudcolored uigetcontrol uisetlabel isfalse istrue iif randint cstr strlen getobjectinternaltype cstrf stopwatchcreate stopwatchstart stopwatchstop stopwatchelapsedseconds cnumber floor ceiling round abs getworldname getitemcountininventorybyname getheading getitemcountininventorybynamerx getheadingto actiontrygiveprofile vitae getfellowshipstatus getfellowshipname getfellowshipisopen getfellowshipisleader getfellowshipleaderid getfellowshipcanrecruit getfellowid getfellowshipcount getfellowshiplocked getfellowname getfellowshipisfull sin cos tan sqrt asin acos atan atan2 sinh cosh tanh vtsetmetastate getregexmatch echo chr ord wobjectgetid wobjectgethealth wobjectfindbyid wobjectgetintprop wobjectfindnearestbytemplatetype wobjectgetopencontainer testquestflag getquestktprogress isrefreshingquests getquestktrequired getqueststatus getisday getgamehour getgamehourname getisnight getgameday getgameticks getminutesuntilday getgamemonth getgamemonthname getminutesuntilnight getgameyear uisetvisible uiviewvisible uiviewexists getgvar touchgvar getpvar touchpvar setgvar cleargvar setpvar clearpvar testgvar clearallgvars testpvar clearallpvars dictgetitem dictcreate dicthaskey dictadditem dictkeys dictremovekey dictvalues dictclear dictsize dictcopy listgetitem listpop listcreate listcontains listremove listadd listindexof listremoveat listinsert listlastindexof listclear listcopy listcount listreverse
|
||||||
|
|
||||||
|
~~
|
||||||
|
~~ File auto-generated by metaf, a program created by Eskarina of Morningthaw/Coldeve.
|
||||||
|
~~ Get metaf here: https://github.com/JJEII/metaf/
|
||||||
|
~~
|
||||||
|
~~ All recognized structural designators:
|
||||||
|
~~ STATE: DO:
|
||||||
|
~~ IF: NAV:
|
||||||
|
~~
|
||||||
|
~~ All recognized CONDITION (IF:) operation keywords:
|
||||||
|
~~ Never NavEmpty MobsInDist_Priority Not
|
||||||
|
~~ Always Death NeedToBuff PSecsInStateGE
|
||||||
|
~~ All VendorOpen NoMobsInDist SecsOnSpellGE
|
||||||
|
~~ Any VendorClosed BlockE BuPercentGE
|
||||||
|
~~ ChatMatch ItemCountLE CellE DistToRteGE
|
||||||
|
~~ MainSlotsLE ItemCountGE IntoPortal Expr
|
||||||
|
~~ SecsInStateGE MobsInDist_Name ExitPortal ChatCapture
|
||||||
|
~~
|
||||||
|
~~ All recognized ACTION (DO:) operation keywords:
|
||||||
|
~~ None EmbedNav ChatExpr SetOpt
|
||||||
|
~~ SetState CallState SetWatchdog CreateView
|
||||||
|
~~ Chat Return ClearWatchdog DestroyView
|
||||||
|
~~ DoAll DoExpr GetOpt DestroyAllViews
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV types:
|
||||||
|
~~ circular follow
|
||||||
|
~~ linear once
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV NODE types:
|
||||||
|
~~ flw vnd
|
||||||
|
~~ pnt ptl
|
||||||
|
~~ rcl tlk
|
||||||
|
~~ pau chk
|
||||||
|
~~ cht jmp
|
||||||
|
~~ prt (deprecated in VTank)
|
||||||
|
~~ }
|
||||||
|
|
||||||
|
STATE: {Default} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[mssortstring,`CV,AS,EQ,TR-,OC,IC,AL-,EP,MX-`]}
|
||||||
|
SetState {SortManaStones}
|
||||||
|
~~ }
|
||||||
|
STATE: {SortManaStones} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ms set source player}
|
||||||
|
Chat {/ms set dest pack 7}
|
||||||
|
Chat {/ms set ocfilter manastone}
|
||||||
|
ChatExpr {`/ms set flags `+getvar[mssortstring]}
|
||||||
|
Chat {/ms start}
|
||||||
|
IF: ChatMatch {^You think\, \"done sorting\!\"$}
|
||||||
|
DO: SetState {SortHealingKits}
|
||||||
|
~~ }
|
||||||
|
STATE: {SortHealingKits} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ms set source player}
|
||||||
|
Chat {/ms set dest pack 7}
|
||||||
|
Chat {/ms set ocfilter healingkit}
|
||||||
|
ChatExpr {`/ms set flags `+getvar[mssortstring]}
|
||||||
|
Chat {/ms start}
|
||||||
|
IF: ChatMatch {^You think\, \"done sorting\!\"$}
|
||||||
|
DO: SetState {SortMisc}
|
||||||
|
~~ }
|
||||||
|
STATE: {SortMisc} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ms set source player}
|
||||||
|
Chat {/ms set dest pack 2}
|
||||||
|
Chat {/ms set ocfilter misc}
|
||||||
|
ChatExpr {`/ms set flags `+getvar[mssortstring]}
|
||||||
|
Chat {/ms start}
|
||||||
|
IF: ChatMatch {^You think\, \"done sorting\!\"$}
|
||||||
|
DO: SetState {SortGems}
|
||||||
|
~~ }
|
||||||
|
STATE: {SortGems} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ms set source player}
|
||||||
|
Chat {/ms set dest pack 3}
|
||||||
|
Chat {/ms set ocfilter gem}
|
||||||
|
ChatExpr {`/ms set flags `+getvar[mssortstring]}
|
||||||
|
Chat {/ms start}
|
||||||
|
IF: ChatMatch {^You think\, \"done sorting\!\"$}
|
||||||
|
DO: SetState {SortWeapons}
|
||||||
|
~~ }
|
||||||
|
STATE: {SortWeapons} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ms set source player}
|
||||||
|
Chat {/ms set dest pack 1}
|
||||||
|
Chat {/ms set ocfilter meleeweapon}
|
||||||
|
ChatExpr {`/ms set flags `+getvar[mssortstring]}
|
||||||
|
Chat {/ms start}
|
||||||
|
IF: ChatMatch {^You think\, \"done sorting\!\"$}
|
||||||
|
DO: SetState {SortWands}
|
||||||
|
~~ }
|
||||||
|
STATE: {SortWands} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ms set source player}
|
||||||
|
Chat {/ms set dest pack 1}
|
||||||
|
Chat {/ms set ocfilter wand}
|
||||||
|
ChatExpr {`/ms set flags `+getvar[mssortstring]}
|
||||||
|
Chat {/ms start}
|
||||||
|
IF: ChatMatch {^You think\, \"done sorting\!\"$}
|
||||||
|
DO: SetState {SortSpellComps}
|
||||||
|
~~ }
|
||||||
|
STATE: {SortSpellComps} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ms set source player}
|
||||||
|
Chat {/ms set dest pack 8}
|
||||||
|
Chat {/ms set ocfilter spellcomp}
|
||||||
|
ChatExpr {`/ms set flags `+getvar[mssortstring]}
|
||||||
|
Chat {/ms start}
|
||||||
|
IF: ChatMatch {^You think\, \"done sorting\!\"$}
|
||||||
|
DO: SetState {SortInventory}
|
||||||
|
~~ }
|
||||||
|
STATE: {SortInventory} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ms set source player}
|
||||||
|
Chat {/ms set dest player}
|
||||||
|
Chat {/ms clear ocfilter}
|
||||||
|
ChatExpr {`/ms set flags `+getvar[mssortstring]}
|
||||||
|
Chat {/ms start}
|
||||||
|
IF: ChatMatch {^You think\, \"done sorting\!\"$}
|
||||||
|
DO: SetState {SortPack1}
|
||||||
|
~~ }
|
||||||
|
STATE: {SortPack1} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ms set source pack 1}
|
||||||
|
Chat {/ms set dest pack 1}
|
||||||
|
Chat {/ms clear ocfilter}
|
||||||
|
ChatExpr {`/ms set flags `+getvar[mssortstring]}
|
||||||
|
Chat {/ms start}
|
||||||
|
IF: ChatMatch {^You think\, \"done sorting\!\"$}
|
||||||
|
DO: SetState {SortPack2}
|
||||||
|
~~ }
|
||||||
|
STATE: {SortPack2} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ms set source pack 2}
|
||||||
|
Chat {/ms set dest pack 2}
|
||||||
|
Chat {/ms clear ocfilter}
|
||||||
|
ChatExpr {`/ms set flags `+getvar[mssortstring]}
|
||||||
|
Chat {/ms start}
|
||||||
|
IF: ChatMatch {^You think\, \"done sorting\!\"$}
|
||||||
|
DO: SetState {SortPack3}
|
||||||
|
~~ }
|
||||||
|
STATE: {SortPack3} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ms set source pack 3}
|
||||||
|
Chat {/ms set dest pack 3}
|
||||||
|
Chat {/ms clear ocfilter}
|
||||||
|
ChatExpr {`/ms set flags `+getvar[mssortstring]}
|
||||||
|
Chat {/ms start}
|
||||||
|
IF: ChatMatch {^You think\, \"done sorting\!\"$}
|
||||||
|
DO: SetState {SortPack4}
|
||||||
|
~~ }
|
||||||
|
STATE: {SortPack4} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ms set source pack 4}
|
||||||
|
Chat {/ms set dest pack 4}
|
||||||
|
Chat {/ms clear ocfilter}
|
||||||
|
ChatExpr {`/ms set flags `+getvar[mssortstring]}
|
||||||
|
Chat {/ms start}
|
||||||
|
IF: ChatMatch {^You think\, \"done sorting\!\"$}
|
||||||
|
DO: SetState {SortPack5}
|
||||||
|
~~ }
|
||||||
|
STATE: {SortPack5} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ms set source pack 5}
|
||||||
|
Chat {/ms set dest pack 5}
|
||||||
|
Chat {/ms clear ocfilter}
|
||||||
|
ChatExpr {`/ms set flags `+getvar[mssortstring]}
|
||||||
|
Chat {/ms start}
|
||||||
|
IF: ChatMatch {^You think\, \"done sorting\!\"$}
|
||||||
|
DO: SetState {SortPack6}
|
||||||
|
~~ }
|
||||||
|
STATE: {SortPack6} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ms set source pack 6}
|
||||||
|
Chat {/ms set dest pack 6}
|
||||||
|
Chat {/ms clear ocfilter}
|
||||||
|
ChatExpr {`/ms set flags `+getvar[mssortstring]}
|
||||||
|
Chat {/ms start}
|
||||||
|
IF: ChatMatch {^You think\, \"done sorting\!\"$}
|
||||||
|
DO: SetState {SortPack7}
|
||||||
|
~~ }
|
||||||
|
STATE: {SortPack7} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ms set source pack 7}
|
||||||
|
Chat {/ms set dest pack 7}
|
||||||
|
Chat {/ms clear ocfilter}
|
||||||
|
ChatExpr {`/ms set flags `+getvar[mssortstring]}
|
||||||
|
Chat {/ms start}
|
||||||
|
IF: ChatMatch {^You think\, \"done sorting\!\"$}
|
||||||
|
DO: SetState {SortPack8}
|
||||||
|
~~ }
|
||||||
|
STATE: {SortPack8} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ms set source pack 8}
|
||||||
|
Chat {/ms set dest pack 8}
|
||||||
|
Chat {/ms clear ocfilter}
|
||||||
|
ChatExpr {`/ms set flags `+getvar[mssortstring]}
|
||||||
|
Chat {/ms start}
|
||||||
|
IF: ChatMatch {^You think\, \"done sorting\!\"$}
|
||||||
|
DO: SetState {AllDone}
|
||||||
|
~~ }
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
~~ {
|
||||||
|
~~ FOR AUTO-COMPLETION ASSISTANCE: testvar getvar setvar touchvar clearallvars clearvar getcharintprop getchardoubleprop getcharquadprop getcharboolprop getcharstringprop getisspellknown getcancastspell_hunt getcancastspell_buff getcharvital_base getcharvital_current getcharvital_buffedmax getcharskill_traininglevel getcharskill_base getcharskill_buffed getplayerlandcell getplayercoordinates coordinategetns coordinategetwe coordinategetz coordinatetostring coordinateparse coordinatedistancewithz coordinatedistanceflat wobjectgetphysicscoordinates wobjectgetname wobjectgetobjectclass wobjectgettemplatetype wobjectgetisdooropen wobjectfindnearestmonster wobjectfindnearestdoor wobjectfindnearestbyobjectclass wobjectfindininventorybytemplatetype wobjectfindininventorybyname wobjectfindininventorybynamerx wobjectgetselection wobjectgetplayer wobjectfindnearestbynameandobjectclass actiontryselect actiontryuseitem actiontryapplyitem actiontrygiveitem actiontryequipanywand actiontrycastbyid actiontrycastbyidontarget chatbox chatboxpaste statushud statushudcolored uigetcontrol uisetlabel isfalse istrue iif randint cstr strlen getobjectinternaltype cstrf stopwatchcreate stopwatchstart stopwatchstop stopwatchelapsedseconds cnumber floor ceiling round abs getworldname getitemcountininventorybyname getheading getitemcountininventorybynamerx getheadingto actiontrygiveprofile vitae getfellowshipstatus getfellowshipname getfellowshipisopen getfellowshipisleader getfellowshipleaderid getfellowshipcanrecruit getfellowid getfellowshipcount getfellowshiplocked getfellowname getfellowshipisfull sin cos tan sqrt asin acos atan atan2 sinh cosh tanh vtsetmetastate getregexmatch echo chr ord wobjectgetid wobjectgethealth wobjectfindbyid wobjectgetintprop wobjectfindnearestbytemplatetype wobjectgetopencontainer testquestflag getquestktprogress isrefreshingquests getquestktrequired getqueststatus getisday getgamehour getgamehourname getisnight getgameday getgameticks getminutesuntilday getgamemonth getgamemonthname getminutesuntilnight getgameyear uisetvisible uiviewvisible uiviewexists getgvar touchgvar getpvar touchpvar setgvar cleargvar setpvar clearpvar testgvar clearallgvars testpvar clearallpvars dictgetitem dictcreate dicthaskey dictadditem dictkeys dictremovekey dictvalues dictclear dictsize dictcopy listgetitem listpop listcreate listcontains listremove listadd listindexof listremoveat listinsert listlastindexof listclear listcopy listcount listreverse
|
||||||
|
|
||||||
|
~~
|
||||||
|
~~ File auto-generated by metaf, a program created by Eskarina of Morningthaw/Coldeve.
|
||||||
|
~~ Get metaf here: https://github.com/JJEII/metaf/
|
||||||
|
~~
|
||||||
|
~~ All recognized structural designators:
|
||||||
|
~~ STATE: DO:
|
||||||
|
~~ IF: NAV:
|
||||||
|
~~
|
||||||
|
~~ All recognized CONDITION (IF:) operation keywords:
|
||||||
|
~~ Never NavEmpty MobsInDist_Priority Not
|
||||||
|
~~ Always Death NeedToBuff PSecsInStateGE
|
||||||
|
~~ All VendorOpen NoMobsInDist SecsOnSpellGE
|
||||||
|
~~ Any VendorClosed BlockE BuPercentGE
|
||||||
|
~~ ChatMatch ItemCountLE CellE DistToRteGE
|
||||||
|
~~ MainSlotsLE ItemCountGE IntoPortal Expr
|
||||||
|
~~ SecsInStateGE MobsInDist_Name ExitPortal ChatCapture
|
||||||
|
~~
|
||||||
|
~~ All recognized ACTION (DO:) operation keywords:
|
||||||
|
~~ None EmbedNav ChatExpr SetOpt
|
||||||
|
~~ SetState CallState SetWatchdog CreateView
|
||||||
|
~~ Chat Return ClearWatchdog DestroyView
|
||||||
|
~~ DoAll DoExpr GetOpt DestroyAllViews
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV types:
|
||||||
|
~~ circular follow
|
||||||
|
~~ linear once
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV NODE types:
|
||||||
|
~~ flw vnd
|
||||||
|
~~ pnt ptl
|
||||||
|
~~ rcl tlk
|
||||||
|
~~ pau chk
|
||||||
|
~~ cht jmp
|
||||||
|
~~ prt (deprecated in VTank)
|
||||||
|
~~ }
|
||||||
|
|
||||||
|
~~ Follower Meta: Follows group leader, responds to chat commands, death recovery
|
||||||
|
|
||||||
|
STATE: {Default} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt opt set enablenav false}
|
||||||
|
Chat {/vt opt set enablebuffing true}
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
DoExpr {setvar[leaderName,]}
|
||||||
|
DoExpr {setvar[deathCount, 0]}
|
||||||
|
SetState {idle}
|
||||||
|
~~ }
|
||||||
|
STATE: {idle} ~~ {
|
||||||
|
~~ Wait for leader assignment via chat command
|
||||||
|
IF: ChatMatch {.+tells you, "follow me"}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[leaderName, getregexmatch[(.+) tells you, 1]]}
|
||||||
|
ChatExpr {\/t +getvar[leaderName]+, Following you now\!}
|
||||||
|
SetState {following}
|
||||||
|
IF: ChatMatch {.+tells you, "follow (.+)"}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[leaderName, getregexmatch[.+tells you, "follow (.+)", 1]]}
|
||||||
|
ChatExpr {\/t +getvar[leaderName]+, Following you now\!}
|
||||||
|
SetState {following}
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death_recovery}
|
||||||
|
IF: NeedToBuff
|
||||||
|
DO: SetState {buffing}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt opt set enablecombat false}
|
||||||
|
~~ }
|
||||||
|
STATE: {following} ~~ {
|
||||||
|
~~ Main follow state - follow the leader
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt opt set enablecombat false}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt opt set enablenav false}
|
||||||
|
IF: Always
|
||||||
|
DO: ChatExpr {\/ub follow +getvar[leaderName]}
|
||||||
|
~~ Death handling
|
||||||
|
IF: Death
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[deathCount, getvar[deathCount] + 1]}
|
||||||
|
ChatExpr {\/t +getvar[leaderName]+, I died\! Death count: +cstr[getvar[deathCount]]+. Recovering...}
|
||||||
|
SetState {death_recovery}
|
||||||
|
~~ Chat command: buff
|
||||||
|
IF: ChatMatch {.+tells you, "buff"}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablebuffing true}
|
||||||
|
SetState {buffing}
|
||||||
|
~~ Chat command: recall
|
||||||
|
IF: ChatMatch {.+tells you, "recall"}
|
||||||
|
DO: DoAll
|
||||||
|
ChatExpr {\/t +getvar[leaderName]+, Recalling now\!}
|
||||||
|
SetState {recalling}
|
||||||
|
~~ Chat command: stop
|
||||||
|
IF: ChatMatch {.+tells you, "stop"}
|
||||||
|
DO: DoAll
|
||||||
|
ChatExpr {\/t +getvar[leaderName]+, Stopping follow\.}
|
||||||
|
DoExpr {setvar[leaderName,]}
|
||||||
|
Chat {/ub follow off}
|
||||||
|
SetState {idle}
|
||||||
|
~~ Chat command: combat on
|
||||||
|
IF: ChatMatch {.+tells you, "combat on"}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
ChatExpr {\/t +getvar[leaderName]+, Combat enabled\!}
|
||||||
|
~~ Chat command: combat off
|
||||||
|
IF: ChatMatch {.+tells you, "combat off"}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
ChatExpr {\/t +getvar[leaderName]+, Combat disabled\!}
|
||||||
|
~~ Need to rebuff
|
||||||
|
IF: NeedToBuff
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ub follow off}
|
||||||
|
SetState {buffing}
|
||||||
|
~~ Watchdog - if stuck for too long, try to recover
|
||||||
|
IF: Always
|
||||||
|
DO: SetWatchdog 3 300 {stuck_recovery}
|
||||||
|
~~ }
|
||||||
|
STATE: {buffing} ~~ {
|
||||||
|
~~ Buff up then return to following
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablebuffing true}
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
IF: Death
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[deathCount, getvar[deathCount] + 1]}
|
||||||
|
SetState {death_recovery}
|
||||||
|
IF: All
|
||||||
|
Not NeedToBuff
|
||||||
|
SecsInStateGE 5
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablebuffing true}
|
||||||
|
SetState {rejoin_leader}
|
||||||
|
IF: SecsInStateGE 120
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablebuffing true}
|
||||||
|
SetState {rejoin_leader}
|
||||||
|
~~ }
|
||||||
|
STATE: {death_recovery} ~~ {
|
||||||
|
~~ Wait at lifestone after death, then rebuff and rejoin
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt opt set enablenav false}
|
||||||
|
Chat {/vt opt set enablebuffing false}
|
||||||
|
~~ Wait a few seconds at lifestone before doing anything
|
||||||
|
IF: SecsInStateGE 10
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablebuffing true}
|
||||||
|
SetState {death_rebuff}
|
||||||
|
~~ }
|
||||||
|
STATE: {death_rebuff} ~~ {
|
||||||
|
~~ Rebuff after death before rejoining
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt opt set enablebuffing true}
|
||||||
|
IF: Death
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[deathCount, getvar[deathCount] + 1]}
|
||||||
|
SetState {death_recovery}
|
||||||
|
IF: All
|
||||||
|
Not NeedToBuff
|
||||||
|
SecsInStateGE 10
|
||||||
|
DO: SetState {rejoin_leader}
|
||||||
|
IF: SecsInStateGE 180
|
||||||
|
DO: SetState {rejoin_leader}
|
||||||
|
~~ }
|
||||||
|
STATE: {rejoin_leader} ~~ {
|
||||||
|
~~ Rejoin the leader after buffing or death recovery
|
||||||
|
IF: Expr {getvar[leaderName] == 0}
|
||||||
|
DO: SetState {idle}
|
||||||
|
IF: Expr {cstr[getvar[leaderName]] == cstr[]}
|
||||||
|
DO: SetState {idle}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
ChatExpr {\/t +getvar[leaderName]+, Rejoining you now\!}
|
||||||
|
ChatExpr {\/ub follow +getvar[leaderName]}
|
||||||
|
SetState {following}
|
||||||
|
~~ }
|
||||||
|
STATE: {recalling} ~~ {
|
||||||
|
~~ Recall to lifestone
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ub follow off}
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt opt set enablenav false}
|
||||||
|
IF: SecsInStateGE 2
|
||||||
|
DO: Chat {/lifestone}
|
||||||
|
IF: Death
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[deathCount, getvar[deathCount] + 1]}
|
||||||
|
SetState {death_recovery}
|
||||||
|
IF: SecsInStateGE 15
|
||||||
|
DO: DoAll
|
||||||
|
ChatExpr {\/t +getvar[leaderName]+, Recalled to lifestone\. Tell me to follow you when ready\.}
|
||||||
|
SetState {idle}
|
||||||
|
~~ }
|
||||||
|
STATE: {stuck_recovery} ~~ {
|
||||||
|
~~ Try to recover from being stuck
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ub follow off}
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
ChatExpr {\/t +getvar[leaderName]+, I appear to be stuck\. Trying to recover\.}
|
||||||
|
IF: SecsInStateGE 3
|
||||||
|
DO: Chat {/lifestone}
|
||||||
|
IF: Death
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[deathCount, getvar[deathCount] + 1]}
|
||||||
|
SetState {death_recovery}
|
||||||
|
IF: SecsInStateGE 15
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablebuffing true}
|
||||||
|
SetState {death_rebuff}
|
||||||
|
~~ }
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
~~ {
|
||||||
|
~~ FOR AUTO-COMPLETION ASSISTANCE: testvar getvar setvar touchvar clearallvars clearvar getcharintprop getchardoubleprop getcharquadprop getcharboolprop getcharstringprop getisspellknown getcancastspell_hunt getcancastspell_buff getcharvital_base getcharvital_current getcharvital_buffedmax getcharskill_traininglevel getcharskill_base getcharskill_buffed getplayerlandcell getplayercoordinates coordinategetns coordinategetwe coordinategetz coordinatetostring coordinateparse coordinatedistancewithz coordinatedistanceflat wobjectgetphysicscoordinates wobjectgetname wobjectgetobjectclass wobjectgettemplatetype wobjectgetisdooropen wobjectfindnearestmonster wobjectfindnearestdoor wobjectfindnearestbyobjectclass wobjectfindininventorybytemplatetype wobjectfindininventorybyname wobjectfindininventorybynamerx wobjectgetselection wobjectgetplayer wobjectfindnearestbynameandobjectclass actiontryselect actiontryuseitem actiontryapplyitem actiontrygiveitem actiontryequipanywand actiontrycastbyid actiontrycastbyidontarget chatbox chatboxpaste statushud statushudcolored uigetcontrol uisetlabel isfalse istrue iif randint cstr strlen getobjectinternaltype cstrf stopwatchcreate stopwatchstart stopwatchstop stopwatchelapsedseconds cnumber floor ceiling round abs getworldname getitemcountininventorybyname getheading getitemcountininventorybynamerx getheadingto actiontrygiveprofile vitae getfellowshipstatus getfellowshipname getfellowshipisopen getfellowshipisleader getfellowshipleaderid getfellowshipcanrecruit getfellowid getfellowshipcount getfellowshiplocked getfellowname getfellowshipisfull sin cos tan sqrt asin acos atan atan2 sinh cosh tanh vtsetmetastate getregexmatch echo chr ord wobjectgetid wobjectgethealth wobjectfindbyid wobjectgetintprop wobjectfindnearestbytemplatetype wobjectgetopencontainer testquestflag getquestktprogress isrefreshingquests getquestktrequired getqueststatus getisday getgamehour getgamehourname getisnight getgameday getgameticks getminutesuntilday getgamemonth getgamemonthname getminutesuntilnight getgameyear uisetvisible uiviewvisible uiviewexists getgvar touchgvar getpvar touchpvar setgvar cleargvar setpvar clearpvar testgvar clearallgvars testpvar clearallpvars dictgetitem dictcreate dicthaskey dictadditem dictkeys dictremovekey dictvalues dictclear dictsize dictcopy listgetitem listpop listcreate listcontains listremove listadd listindexof listremoveat listinsert listlastindexof listclear listcopy listcount listreverse
|
||||||
|
|
||||||
|
~~
|
||||||
|
~~ File auto-generated by metaf, a program created by Eskarina of Morningthaw/Coldeve.
|
||||||
|
~~ Get metaf here: https://github.com/JJEII/metaf/
|
||||||
|
~~
|
||||||
|
~~ All recognized structural designators:
|
||||||
|
~~ STATE: DO:
|
||||||
|
~~ IF: NAV:
|
||||||
|
~~
|
||||||
|
~~ All recognized CONDITION (IF:) operation keywords:
|
||||||
|
~~ Never NavEmpty MobsInDist_Priority Not
|
||||||
|
~~ Always Death NeedToBuff PSecsInStateGE
|
||||||
|
~~ All VendorOpen NoMobsInDist SecsOnSpellGE
|
||||||
|
~~ Any VendorClosed BlockE BuPercentGE
|
||||||
|
~~ ChatMatch ItemCountLE CellE DistToRteGE
|
||||||
|
~~ MainSlotsLE ItemCountGE IntoPortal Expr
|
||||||
|
~~ SecsInStateGE MobsInDist_Name ExitPortal ChatCapture
|
||||||
|
~~
|
||||||
|
~~ All recognized ACTION (DO:) operation keywords:
|
||||||
|
~~ None EmbedNav ChatExpr SetOpt
|
||||||
|
~~ SetState CallState SetWatchdog CreateView
|
||||||
|
~~ Chat Return ClearWatchdog DestroyView
|
||||||
|
~~ DoAll DoExpr GetOpt DestroyAllViews
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV types:
|
||||||
|
~~ circular follow
|
||||||
|
~~ linear once
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV NODE types:
|
||||||
|
~~ flw vnd
|
||||||
|
~~ pnt ptl
|
||||||
|
~~ rcl tlk
|
||||||
|
~~ pau chk
|
||||||
|
~~ cht jmp
|
||||||
|
~~ prt (deprecated in VTank)
|
||||||
|
~~ }
|
||||||
|
|
||||||
|
STATE: {all_run_back} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /vt setmetastate just_run_back}
|
||||||
|
~~ }
|
||||||
|
STATE: {strength} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ub bc /ub use Luminous Crystal of Surging Strength}
|
||||||
|
SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {closestportal} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /ub closestportal}
|
||||||
|
IF: Always
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {death} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt nav load death_pause}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt opt set enablenav true}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {run_back}
|
||||||
|
~~ }
|
||||||
|
STATE: {Default} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: None
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
~~ }
|
||||||
|
STATE: {defence} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ub bc /ub use Luminous Crystal of Towering Defense}
|
||||||
|
SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {dispell} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ub bc /ub use Black Market Gem of Dispelling}
|
||||||
|
SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {follow} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
ChatExpr {\/ub bc \/ub follow +wobjectgetname[wobjectgetplayer[]]}
|
||||||
|
Chat {/ub bc /vt opt set enablenav true}
|
||||||
|
IF: Always
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {jump} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /ub jumpsw 400}
|
||||||
|
IF: Always
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {just_run_back} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load gauntlet_just_run}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
Chat {/vt opt set enablebuffing true}
|
||||||
|
Chat {/vt opt set enablemeta true}
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {load_follow} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /vt meta load gauntlet_follower}
|
||||||
|
IF: Always
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {nav_false} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /vt opt set enablenav false}
|
||||||
|
IF: Always
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {nav_true} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /vt opt set enablenav true}
|
||||||
|
IF: Always
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {run_back} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load gauntlet_run}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
Chat {/vt opt set enablebuffing true}
|
||||||
|
Chat {/vt opt set enablemeta true}
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {setup} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: ChatExpr {\/ub bc \/ub follow +wobjectgetname[wobjectgetplayer[]]}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /vt opt set enablelooting false}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /vt opt set enablenav true}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /vt opt set enablebuffing true}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /vt opt set enablecombat true}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /vt opt set attackdistance 0.125}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /vt opt set navpriorityboost true}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /vt opt set buffprofile_prots 2}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /vt opt set buffprofile_banes 2}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /vt forcebuff}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /vt start}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /vt opt set enablemeta true}
|
||||||
|
IF: Always
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {summon_false} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /vt opt set SummonPets false}
|
||||||
|
IF: Always
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
STATE: {summon_true} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub bc /vt opt set SummonPets true}
|
||||||
|
IF: Always
|
||||||
|
DO: SetState {Default}
|
||||||
|
~~ }
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
~~ {
|
||||||
|
~~ Hunting Meta - Solo lifestone grinding with death recovery, restock, and stuck detection
|
||||||
|
~~
|
||||||
|
~~ File auto-generated by metaf, a program created by Eskarina of Morningthaw/Coldeve.
|
||||||
|
~~ Get metaf here: https://github.com/JJEII/metaf/
|
||||||
|
~~
|
||||||
|
~~ All recognized structural designators:
|
||||||
|
~~ STATE: DO:
|
||||||
|
~~ IF: NAV:
|
||||||
|
~~
|
||||||
|
~~ All recognized CONDITION (IF:) operation keywords:
|
||||||
|
~~ Never NavEmpty MobsInDist_Priority Not
|
||||||
|
~~ Always Death NeedToBuff PSecsInStateGE
|
||||||
|
~~ All VendorOpen NoMobsInDist SecsOnSpellGE
|
||||||
|
~~ Any VendorClosed BlockE BuPercentGE
|
||||||
|
~~ ChatMatch ItemCountLE CellE DistToRteGE
|
||||||
|
~~ MainSlotsLE ItemCountGE IntoPortal Expr
|
||||||
|
~~ SecsInStateGE MobsInDist_Name ExitPortal ChatCapture
|
||||||
|
~~
|
||||||
|
~~ All recognized ACTION (DO:) operation keywords:
|
||||||
|
~~ None EmbedNav ChatExpr SetOpt
|
||||||
|
~~ SetState CallState SetWatchdog CreateView
|
||||||
|
~~ Chat Return ClearWatchdog DestroyView
|
||||||
|
~~ DoAll DoExpr GetOpt DestroyAllViews
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV types:
|
||||||
|
~~ circular follow
|
||||||
|
~~ linear once
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV NODE types:
|
||||||
|
~~ flw vnd
|
||||||
|
~~ pnt ptl
|
||||||
|
~~ rcl tlk
|
||||||
|
~~ pau chk
|
||||||
|
~~ cht jmp
|
||||||
|
~~ prt (deprecated in VTank)
|
||||||
|
~~ }
|
||||||
|
|
||||||
|
STATE: {Default} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablebuffing true}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
Chat {/vt opt set enablelooting true}
|
||||||
|
Chat {/vt opt set navpriorityboost false}
|
||||||
|
Chat {/vt opt set lootpriorityboost true}
|
||||||
|
Chat {/vt nav load hunt_circuit}
|
||||||
|
SetState {Hunt}
|
||||||
|
~~ }
|
||||||
|
STATE: {Hunt} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {Death}
|
||||||
|
IF: All
|
||||||
|
ItemCountLE 20 {Prismatic Taper}
|
||||||
|
NoMobsInDist 5
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a Low on tapers, heading to vendor.}
|
||||||
|
SetState {Restock}
|
||||||
|
IF: All
|
||||||
|
ItemCountLE 20 {Scarab}
|
||||||
|
NoMobsInDist 5
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a Low on scarabs, heading to vendor.}
|
||||||
|
SetState {Restock}
|
||||||
|
IF: MainSlotsLE 3
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a Inventory full, heading to vendor.}
|
||||||
|
SetState {Restock}
|
||||||
|
IF: NeedToBuff
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablenav false}
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
SetState {Buffing}
|
||||||
|
IF: Always
|
||||||
|
DO: SetWatchdog 3 120 {Stuck}
|
||||||
|
IF: DistToRteGE 500
|
||||||
|
DO: SetState {Stuck}
|
||||||
|
IF: MobsInDist_Name 1 12 {}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
IF: NoMobsInDist 12
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load hunt_circuit}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
~~ }
|
||||||
|
STATE: {Death} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt opt set enablenav false}
|
||||||
|
Chat {/a I died! Recovering...}
|
||||||
|
IF: SecsInStateGE 5
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablebuffing true}
|
||||||
|
SetState {DeathRecovery}
|
||||||
|
~~ }
|
||||||
|
STATE: {DeathRecovery} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load ls_to_hunt}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
IF: NeedToBuff
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablenav false}
|
||||||
|
SetState {Buffing}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load hunt_circuit}
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
SetState {Hunt}
|
||||||
|
IF: Always
|
||||||
|
DO: SetWatchdog 3 300 {Stuck}
|
||||||
|
~~ }
|
||||||
|
STATE: {Buffing} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablebuffing true}
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt opt set enablenav false}
|
||||||
|
IF: All
|
||||||
|
Not NeedToBuff
|
||||||
|
NoMobsInDist 5
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
SetState {Hunt}
|
||||||
|
IF: SecsInStateGE 180
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
SetState {Hunt}
|
||||||
|
~~ }
|
||||||
|
STATE: {Restock} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {Death}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
Chat {/vt nav load hunt_to_vendor}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
IF: Always
|
||||||
|
DO: SetWatchdog 3 300 {Stuck}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: SetState {AtVendor}
|
||||||
|
~~ }
|
||||||
|
STATE: {AtVendor} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {Death}
|
||||||
|
IF: VendorOpen
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablenav false}
|
||||||
|
IF: All
|
||||||
|
VendorClosed
|
||||||
|
ItemCountGE 200 {Prismatic Taper}
|
||||||
|
ItemCountGE 200 {Scarab}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a Restocked, heading back to hunt.}
|
||||||
|
Chat {/vt nav load vendor_to_hunt}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
SetState {ReturnToHunt}
|
||||||
|
IF: SecsInStateGE 120
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a Vendor timeout, returning to hunt.}
|
||||||
|
Chat {/vt nav load vendor_to_hunt}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
SetState {ReturnToHunt}
|
||||||
|
~~ }
|
||||||
|
STATE: {ReturnToHunt} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {Death}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
IF: Always
|
||||||
|
DO: SetWatchdog 3 300 {Stuck}
|
||||||
|
IF: NeedToBuff
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablenav false}
|
||||||
|
SetState {Buffing}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load hunt_circuit}
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
Chat {/vt opt set enablelooting true}
|
||||||
|
SetState {Hunt}
|
||||||
|
~~ }
|
||||||
|
STATE: {Stuck} ~~ {
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a I appear to be stuck, attempting recovery.}
|
||||||
|
Chat {/vt opt set enablenav false}
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
IF: SecsInStateGE 3
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load ls_to_hunt}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
SetState {StuckRecovery}
|
||||||
|
~~ }
|
||||||
|
STATE: {StuckRecovery} ~~ {
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {Death}
|
||||||
|
IF: Always
|
||||||
|
DO: SetWatchdog 3 300 {Stuck}
|
||||||
|
IF: NeedToBuff
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablenav false}
|
||||||
|
SetState {Buffing}
|
||||||
|
IF: NavEmpty
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt nav load hunt_circuit}
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
Chat {/vt opt set enablelooting true}
|
||||||
|
SetState {Hunt}
|
||||||
|
~~ }
|
||||||
|
|
@ -0,0 +1,416 @@
|
||||||
|
~~
|
||||||
|
~~ lockandkey.af - Portal Bot with Stipend & Partial Bella Services
|
||||||
|
~~
|
||||||
|
~~ A portal summoning bot that:
|
||||||
|
~~ - Listens for tells containing "primary" or "secondary" to summon portals
|
||||||
|
~~ - Collects stipend on timer (with backoff)
|
||||||
|
~~ - Runs partial bella (jaw turn-in only, no kill bella)
|
||||||
|
~~ - Returns home via ah_recall after services
|
||||||
|
~~
|
||||||
|
~~ Portal Spells:
|
||||||
|
~~ Summon Primary Portal I = spell 157
|
||||||
|
~~ Summon Secondary Portal I = spell 2648
|
||||||
|
~~
|
||||||
|
|
||||||
|
~~ {
|
||||||
|
~~ FOR AUTO-COMPLETION ASSISTANCE: testvar getvar setvar touchvar clearallvars clearvar getcharintprop getchardoubleprop getcharquadprop getcharboolprop getcharstringprop getisspellknown getcancastspell_hunt getcancastspell_buff getcharvital_base getcharvital_current getcharvital_buffedmax getcharskill_traininglevel getcharskill_base getcharskill_buffed getplayerlandcell getplayercoordinates coordinategetns coordinategetwe coordinategetz coordinatetostring coordinateparse coordinatedistancewithz coordinatedistanceflat wobjectgetphysicscoordinates wobjectgetname wobjectgetobjectclass wobjectgettemplatetype wobjectgetisdooropen wobjectfindnearestmonster wobjectfindnearestdoor wobjectfindnearestbyobjectclass wobjectfindininventorybytemplatetype wobjectfindininventorybyname wobjectfindininventorybynamerx wobjectgetselection wobjectgetplayer wobjectfindnearestbynameandobjectclass actiontryselect actiontryuseitem actiontryapplyitem actiontrygiveitem actiontryequipanywand actiontrycastbyid actiontrycastbyidontarget chatbox chatboxpaste statushud statushudcolored uigetcontrol uisetlabel isfalse istrue iif randint cstr strlen getobjectinternaltype cstrf stopwatchcreate stopwatchstart stopwatchstop stopwatchelapsedseconds cnumber floor ceiling round abs getworldname getitemcountininventorybyname getheading getitemcountininventorybynamerx getheadingto actiontrygiveprofile vitae getfellowshipstatus getfellowshipname getfellowshipisopen getfellowshipisleader getfellowshipleaderid getfellowshipcanrecruit getfellowid getfellowshipcount getfellowshiplocked getfellowname getfellowshipisfull sin cos tan sqrt asin acos atan atan2 sinh cosh tanh vtsetmetastate getregexmatch echo chr ord wobjectgetid wobjectgethealth wobjectfindbyid wobjectgetintprop wobjectfindnearestbytemplatetype wobjectgetopencontainer testquestflag getquestktprogress isrefreshingquests getquestktrequired getqueststatus getisday getgamehour getgamehourname getisnight getgameday getgameticks getminutesuntilday getgamemonth getplayerlandblock wobjectisvalid getitemcountbytemplatetype ifthen
|
||||||
|
~~ }
|
||||||
|
|
||||||
|
STATE: {Default}
|
||||||
|
~~ Init variables then transition to service check
|
||||||
|
IF: Not Expr {testvar[bellaTurnInAttempts]}
|
||||||
|
DO: DoExpr {setvar[bellaTurnInAttempts, 0]}
|
||||||
|
IF: Not Expr {testvar[bellaJawRunRecoverAttempts]}
|
||||||
|
DO: DoExpr {setvar[bellaJawRunRecoverAttempts, 0]}
|
||||||
|
IF: Not Expr {testvar[bellaBackoffSeconds]}
|
||||||
|
DO: DoExpr {setvar[bellaBackoffSeconds, 86400]}
|
||||||
|
IF: Not Expr {testvar[stipendBackoffSeconds]}
|
||||||
|
DO: DoExpr {setvar[stipendBackoffSeconds, 86400]}
|
||||||
|
IF: Not Expr {testvar[disableBellaServices]}
|
||||||
|
DO: DoExpr {setvar[disableBellaServices, 0]}
|
||||||
|
IF: Not Expr {testvar[serviceCheckInterval]}
|
||||||
|
DO: DoExpr {setvar[serviceCheckInterval, 900]}
|
||||||
|
IF: Not Expr {testvar[serviceClock]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[serviceClock,stopwatchcreate[]]}
|
||||||
|
DoExpr {stopwatchstart[getvar[serviceClock]]}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ub opt set VTank.PatchExpressionEngine true}
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
Chat {/vt opt set enablenav false}
|
||||||
|
Chat {/vt opt set enablebuffing false}
|
||||||
|
SetState {service_quest_refresh}
|
||||||
|
~~ }
|
||||||
|
|
||||||
|
STATE: {idle}
|
||||||
|
~~ Main loop: listen for portal tells, check service timer
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablebuffing false}
|
||||||
|
DoExpr {setcombatstate[`peace`]}
|
||||||
|
DoExpr {clearvar[queuePrimary]}
|
||||||
|
DoExpr {clearvar[queueSecondary]}
|
||||||
|
~~ Listen for tells containing "primary"
|
||||||
|
IF: ChatCapture {(^(\[[A-z]+?\] |)You|.*\<Tell:IIDString:.+:(?<who>[^\<]*)\>.+\<\\Tell\>) tells you, ".*primary.*"$} {}
|
||||||
|
DO: SetState {summon_primary}
|
||||||
|
~~ Listen for tells containing "secondary"
|
||||||
|
IF: ChatCapture {(^(\[[A-z]+?\] |)You|.*\<Tell:IIDString:.+:(?<who>[^\<]*)\>.+\<\\Tell\>) tells you, ".*secondary.*"$} {}
|
||||||
|
DO: SetState {summon_secondary}
|
||||||
|
~~ Periodic service check
|
||||||
|
IF: All
|
||||||
|
Expr {testvar[serviceClock]}
|
||||||
|
Expr {stopwatchelapsedseconds[getvar[serviceClock]]>=getvar[serviceCheckInterval]}
|
||||||
|
DO: SetState {service_quest_refresh}
|
||||||
|
IF: Always
|
||||||
|
DO: None
|
||||||
|
|
||||||
|
STATE: {summon_primary}
|
||||||
|
~~ Face 180, equip wand, cast Summon Primary Portal I (157), retry on fizzle
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a Summoning 125 Eaters portal...}
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/ub face 180}
|
||||||
|
DoExpr {setcombatstate[`magic`]}
|
||||||
|
DoExpr {actiontryequipanywand[]}
|
||||||
|
DoExpr {clearvar[queuePrimary]}
|
||||||
|
IF: SecsInStateGE 4
|
||||||
|
DO: DoExpr {actiontrycastbyid[157]}
|
||||||
|
~~ Fizzle: retry cast
|
||||||
|
IF: ChatMatch {Your spell fizzled.}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a Fizzled! Retrying 125 Eaters portal...}
|
||||||
|
DoExpr {actiontrycastbyid[157]}
|
||||||
|
SetState {summon_primary}
|
||||||
|
~~ Queue incoming tells while casting
|
||||||
|
IF: ChatCapture {(^(\[[A-z]+?\] |)You|.*\<Tell:IIDString:.+:(?<who>[^\<]*)\>.+\<\\Tell\>) tells you, ".*primary.*"$} {}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a Queued 125 Eaters portal, will summon shortly.}
|
||||||
|
DoExpr {setvar[queuePrimary, 1]}
|
||||||
|
IF: ChatCapture {(^(\[[A-z]+?\] |)You|.*\<Tell:IIDString:.+:(?<who>[^\<]*)\>.+\<\\Tell\>) tells you, ".*secondary.*"$} {}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a Queued Bellas portal, will summon shortly.}
|
||||||
|
DoExpr {setvar[queueSecondary, 1]}
|
||||||
|
IF: SecsInStateGE 17
|
||||||
|
DO: SetState {summon_done}
|
||||||
|
|
||||||
|
STATE: {summon_secondary}
|
||||||
|
~~ Face 90, equip wand, cast Summon Secondary Portal I (2648), retry on fizzle
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a Summoning Bellas portal...}
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/ub face 90}
|
||||||
|
DoExpr {setcombatstate[`magic`]}
|
||||||
|
DoExpr {actiontryequipanywand[]}
|
||||||
|
DoExpr {clearvar[queueSecondary]}
|
||||||
|
IF: SecsInStateGE 4
|
||||||
|
DO: DoExpr {actiontrycastbyid[2648]}
|
||||||
|
~~ Fizzle: retry cast
|
||||||
|
IF: ChatMatch {Your spell fizzled.}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a Fizzled! Retrying Bellas portal...}
|
||||||
|
DoExpr {actiontrycastbyid[2648]}
|
||||||
|
SetState {summon_secondary}
|
||||||
|
~~ Queue incoming tells while casting
|
||||||
|
IF: ChatCapture {(^(\[[A-z]+?\] |)You|.*\<Tell:IIDString:.+:(?<who>[^\<]*)\>.+\<\\Tell\>) tells you, ".*primary.*"$} {}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a Queued 125 Eaters portal, will summon shortly.}
|
||||||
|
DoExpr {setvar[queuePrimary, 1]}
|
||||||
|
IF: ChatCapture {(^(\[[A-z]+?\] |)You|.*\<Tell:IIDString:.+:(?<who>[^\<]*)\>.+\<\\Tell\>) tells you, ".*secondary.*"$} {}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a Queued Bellas portal, will summon shortly.}
|
||||||
|
DoExpr {setvar[queueSecondary, 1]}
|
||||||
|
IF: SecsInStateGE 17
|
||||||
|
DO: SetState {summon_done}
|
||||||
|
|
||||||
|
STATE: {summon_done}
|
||||||
|
~~ Check queue: process next summon request or return to idle
|
||||||
|
IF: Expr {testvar[queuePrimary]}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a Processing queued 125 Eaters portal...}
|
||||||
|
SetState {summon_primary}
|
||||||
|
IF: Expr {testvar[queueSecondary]}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a Processing queued Bellas portal...}
|
||||||
|
SetState {summon_secondary}
|
||||||
|
IF: Always
|
||||||
|
DO: SetState {idle}
|
||||||
|
|
||||||
|
STATE: {service_quest_refresh}
|
||||||
|
~~ Request quest status refresh, wait for completion
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/myquests}
|
||||||
|
DoExpr {touchvar[questsRequested]}
|
||||||
|
IF: All
|
||||||
|
Expr {testvar[questsRequested]}
|
||||||
|
Expr {isrefreshingquests[]==0}
|
||||||
|
SecsInStateGE 2
|
||||||
|
DO: SetState {service_pending_eval}
|
||||||
|
IF: SecsInStateGE 8
|
||||||
|
DO: SetState {service_pending_eval}
|
||||||
|
|
||||||
|
STATE: {service_pending_eval}
|
||||||
|
~~ Check if bella jaw quest is ready (no augment application for portal bot)
|
||||||
|
IF: Always
|
||||||
|
DO: SetState {service_decide}
|
||||||
|
|
||||||
|
STATE: {service_decide}
|
||||||
|
~~ Decide which services to run: stipend, partial bella, or return to idle
|
||||||
|
~~ Reset bella backoff if timer expired
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[disableBellaServices]==1}
|
||||||
|
Expr {testvar[bellaBackoffClock]}
|
||||||
|
Expr {stopwatchelapsedseconds[getvar[bellaBackoffClock]]>=getvar[bellaBackoffSeconds]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[disableBellaServices, 0]}
|
||||||
|
DoExpr {setvar[bellaTurnInAttempts, 0]}
|
||||||
|
DoExpr {setvar[bellaJawRunRecoverAttempts, 0]}
|
||||||
|
DoExpr {clearvar[bellaBackoffClock]}
|
||||||
|
~~ Stipend: first run (no backoff clock)
|
||||||
|
IF: All
|
||||||
|
Expr {getqueststatus[`stipendtimer_0812`]==1}
|
||||||
|
Not Expr {testvar[stipendBackoffClock]}
|
||||||
|
DO: SetState {service_stipend}
|
||||||
|
~~ Stipend: backoff expired
|
||||||
|
IF: All
|
||||||
|
Expr {getqueststatus[`stipendtimer_0812`]==1}
|
||||||
|
Expr {testvar[stipendBackoffClock]}
|
||||||
|
Expr {stopwatchelapsedseconds[getvar[stipendBackoffClock]]>=getvar[stipendBackoffSeconds]}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {clearvar[stipendBackoffClock]}
|
||||||
|
SetState {service_stipend}
|
||||||
|
~~ Partial bella: jaw quest ready + services not disabled
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[disableBellaServices]!=1}
|
||||||
|
Expr {getqueststatus[`insatiableeaterjaw`]==1}
|
||||||
|
DO: SetState {service_bella_jaw_entry}
|
||||||
|
~~ Nothing to do, return to idle
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[serviceClock,stopwatchcreate[]]}
|
||||||
|
DoExpr {stopwatchstart[getvar[serviceClock]]}
|
||||||
|
SetState {idle}
|
||||||
|
|
||||||
|
STATE: {service_stipend}
|
||||||
|
~~ Run stipend collection nav, start backoff clock
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Always
|
||||||
|
DO: SetWatchdog 3 600 {service_reset_main}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
DoExpr {setvar[stipendBackoffClock, stopwatchcreate[]]}
|
||||||
|
DoExpr {stopwatchstart[getvar[stipendBackoffClock]]}
|
||||||
|
DoExpr {touchvar[stipendNavLoaded]}
|
||||||
|
EmbedNav nav0__stipend_nav {stipend.nav}
|
||||||
|
IF: All
|
||||||
|
Expr {testvar[stipendNavLoaded]}
|
||||||
|
NavEmpty
|
||||||
|
SecsInStateGE 2
|
||||||
|
DO: SetState {service_reset_main}
|
||||||
|
|
||||||
|
STATE: {service_bella_jaw_entry}
|
||||||
|
~~ Equip quest gear, buff up, then run nav_lockandkeyjaw (nav handles idle/quest swap + recall)
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a [bella] equipping quest gear and buffing for jaw dungeon}
|
||||||
|
Chat {/ub equip load quest.utl}
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
Chat {/vt opt set enablebuffing true}
|
||||||
|
DoExpr {setvar[jawEntryNavLoaded,1]}
|
||||||
|
Chat {/vt nav load nav_lockandkeyjaw}
|
||||||
|
SetWatchdog 3 600 {service_bella_backoff}
|
||||||
|
IF: All
|
||||||
|
Expr {getvar[jawEntryNavLoaded]==1}
|
||||||
|
NavEmpty
|
||||||
|
SecsInStateGE 2
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[jawEntryNavLoaded,0]}
|
||||||
|
DoExpr {setvar[bellaJawRunRecoverAttempts, 0]}
|
||||||
|
Chat {/vt nav load jaw_1_hunt}
|
||||||
|
Chat {/vt opt set enablecombat true}
|
||||||
|
Chat {/vt opt set enablelooting true}
|
||||||
|
Chat {/vt opt set lootonlyrarecorpses false}
|
||||||
|
SetState {service_bella_jaw_hunt}
|
||||||
|
|
||||||
|
STATE: {service_bella_jaw_run_recover}
|
||||||
|
~~ Recovery wait for jaw run stuck
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: SecsInStateGE 20
|
||||||
|
DO: SetState {service_bella_jaw_entry}
|
||||||
|
|
||||||
|
STATE: {service_bella_jaw_hunt}
|
||||||
|
~~ Hunt for jaw drop
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/vt opt set enablebuffing true}
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
SetState {service_bella_turn_in_jaw}
|
||||||
|
IF: ItemCountGE 1 {Insatiable Eater Jaw}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt nav load to_fiun}
|
||||||
|
IF: SecsInStateGE 1200
|
||||||
|
DO: SetState {service_bella_backoff}
|
||||||
|
|
||||||
|
STATE: {service_bella_turn_in_jaw}
|
||||||
|
~~ Turn in jaw to Fiun NPC, then return home (partial bella - no mp_trans/secondary/kill)
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
~~ Jaw accepted: return home instead of continuing to mp_secondary
|
||||||
|
IF: ChatMatch {^.*One who obtains such as this is truly worthy of that which we would teach\. The highest peak of the deadliest isle contains that which you seek.*$}
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a [bella] jaw turned in, returning home (partial bella)}
|
||||||
|
DoExpr {setvar[bellaTurnInAttempts, 0]}
|
||||||
|
SetState {equip_idle}
|
||||||
|
IF: All
|
||||||
|
SecsInStateGE 300
|
||||||
|
ItemCountGE 1 {Insatiable Eater Jaw}
|
||||||
|
Expr {getvar[bellaTurnInAttempts]<3}
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {chatbox[`/a [bella] jaw turn-in timed out, retry `+cstr[getvar[bellaTurnInAttempts]+1]+`/3`]}
|
||||||
|
DoExpr {setvar[bellaTurnInAttempts,getvar[bellaTurnInAttempts]+1]}
|
||||||
|
Chat {/vt nav load to_fiun}
|
||||||
|
SetState {service_bella_turn_in_jaw}
|
||||||
|
IF: All
|
||||||
|
SecsInStateGE 300
|
||||||
|
ItemCountGE 1 {Insatiable Eater Jaw}
|
||||||
|
Expr {getvar[bellaTurnInAttempts]>=3}
|
||||||
|
DO: SetState {service_bella_backoff}
|
||||||
|
~~ Jaw already gone (consumed by NPC), just go home
|
||||||
|
IF: SecsInStateGE 300
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {setvar[bellaTurnInAttempts, 0]}
|
||||||
|
SetState {equip_idle}
|
||||||
|
|
||||||
|
STATE: {service_bella_backoff}
|
||||||
|
~~ Disable bella services for 24h, return home
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/a [bella] backoff tripped, disabling bella services for 24h}
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
DoExpr {setvar[disableBellaServices, 1]}
|
||||||
|
DoExpr {setvar[bellaTurnInAttempts, 0]}
|
||||||
|
DoExpr {setvar[bellaJawRunRecoverAttempts, 0]}
|
||||||
|
DoExpr {setvar[bellaBackoffClock,stopwatchcreate[]]}
|
||||||
|
DoExpr {stopwatchstart[getvar[bellaBackoffClock]]}
|
||||||
|
SetState {equip_idle}
|
||||||
|
|
||||||
|
STATE: {equip_idle}
|
||||||
|
~~ Swap back to idle gear before heading home
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Always
|
||||||
|
DO: Chat {/ub equip load idle.utl}
|
||||||
|
IF: SecsInStateGE 12
|
||||||
|
DO: SetState {service_reset_main}
|
||||||
|
|
||||||
|
STATE: {service_reset_main}
|
||||||
|
~~ Clear service vars, load ah_recall to return home, then go to idle
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: Always
|
||||||
|
DO: SetWatchdog 3 600 {service_reset_main_stuck}
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {clearvar[questsRequested]}
|
||||||
|
DoExpr {clearvar[stipendNavLoaded]}
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
Chat {/vt opt set enablenav true}
|
||||||
|
Chat {/vt opt set enablebuffing false}
|
||||||
|
DoExpr {setvar[serviceClock,stopwatchcreate[]]}
|
||||||
|
DoExpr {stopwatchstart[getvar[serviceClock]]}
|
||||||
|
DoExpr {touchvar[resetNavLoaded]}
|
||||||
|
Chat {/vt nav load ah_recall}
|
||||||
|
IF: All
|
||||||
|
Expr {testvar[resetNavLoaded]}
|
||||||
|
NavEmpty
|
||||||
|
SecsInStateGE 2
|
||||||
|
DO: DoAll
|
||||||
|
DoExpr {clearvar[resetNavLoaded]}
|
||||||
|
ClearWatchdog
|
||||||
|
Chat {/vt opt set enablenav false}
|
||||||
|
SetState {idle}
|
||||||
|
|
||||||
|
STATE: {service_reset_main_stuck}
|
||||||
|
~~ Fallback if ah_recall nav gets stuck
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ah}
|
||||||
|
SetState {service_reset_main_wait}
|
||||||
|
|
||||||
|
STATE: {service_reset_main_wait}
|
||||||
|
~~ Wait for /ah recall, then retry reset
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: SetState {service_reset_main}
|
||||||
|
IF: SecsInStateGE 30
|
||||||
|
DO: SetState {service_reset_main}
|
||||||
|
|
||||||
|
STATE: {death}
|
||||||
|
~~ Death recovery: wait for portal exit, recall home
|
||||||
|
IF: Always
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/vt opt set enablecombat false}
|
||||||
|
Chat {/vt opt set enablelooting false}
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ah}
|
||||||
|
SetState {death_wait_recall}
|
||||||
|
IF: SecsInStateGE 30
|
||||||
|
DO: DoAll
|
||||||
|
Chat {/ah}
|
||||||
|
SetState {death_wait_recall}
|
||||||
|
|
||||||
|
STATE: {death_wait_recall}
|
||||||
|
~~ Wait for /ah recall portal exit, then return to idle
|
||||||
|
IF: Death
|
||||||
|
DO: SetState {death}
|
||||||
|
IF: ExitPortal
|
||||||
|
DO: SetState {idle}
|
||||||
|
IF: SecsInStateGE 60
|
||||||
|
DO: SetState {idle}
|
||||||
|
|
||||||
|
~~========================= ONLY NAVS APPEAR BELOW THIS LINE =========================~~
|
||||||
|
|
||||||
|
NAV: nav0__stipend_nav once
|
||||||
|
pau 47.1262349446615 26.1864453474681 0.225020837783813 2000
|
||||||
|
cht -101.597905190786 -96.6216093699137 2.08333134651184E-05 {/ah}
|
||||||
|
pau 47.1262349446615 26.1864453474681 0.225020837783813 15000
|
||||||
|
pnt 59.3590666453044 -28.7057823816935 0.0500208298365275
|
||||||
|
ptl -101.597905190786 -96.6216093699137 2.08333134651184E-05 59.3936458587647 -28.7256083488464 0.0508250035345554 14 {Portal to Town Network}
|
||||||
|
pnt -101.615851815542 -96.6388638178507 2.08333134651184E-05
|
||||||
|
pnt -101.657751337687 -96.5832635879517 2.08333134651184E-05
|
||||||
|
pnt -101.658352184296 -96.5325949986776 2.08333134651184E-05
|
||||||
|
ptl -101.597905190786 -96.6216093699137 2.08333134651184E-05 -101.588099161784 -96.5166525046031 -0.000262499845121056 14 {Portal to Arwic}
|
||||||
|
pnt 56.6498762130737 33.416518386205 0.175020837783813
|
||||||
|
pnt 56.655900033315 33.5368880271912 0.175020837783813
|
||||||
|
pnt 56.7470087051392 33.5495386441549 0.175020837783813
|
||||||
|
pnt 56.7795230229696 33.6337207794189 0.175020837783813
|
||||||
|
tlk -101.597905190786 -96.6216093699137 2.08333134651184E-05 56.7816291809082 33.6455291748047 0.175020843744278 37 {Monroe}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
~~ {
|
||||||
|
~~ FOR AUTO-COMPLETION ASSISTANCE: testvar getvar setvar touchvar clearallvars clearvar getcharintprop getchardoubleprop getcharquadprop getcharboolprop getcharstringprop getisspellknown getcancastspell_hunt getcancastspell_buff getcharvital_base getcharvital_current getcharvital_buffedmax getcharskill_traininglevel getcharskill_base getcharskill_buffed getplayerlandcell getplayercoordinates coordinategetns coordinategetwe coordinategetz coordinatetostring coordinateparse coordinatedistancewithz coordinatedistanceflat wobjectgetphysicscoordinates wobjectgetname wobjectgetobjectclass wobjectgettemplatetype wobjectgetisdooropen wobjectfindnearestmonster wobjectfindnearestdoor wobjectfindnearestbyobjectclass wobjectfindininventorybytemplatetype wobjectfindininventorybyname wobjectfindininventorybynamerx wobjectgetselection wobjectgetplayer wobjectfindnearestbynameandobjectclass actiontryselect actiontryuseitem actiontryapplyitem actiontrygiveitem actiontryequipanywand actiontrycastbyid actiontrycastbyidontarget chatbox chatboxpaste statushud statushudcolored uigetcontrol uisetlabel isfalse istrue iif randint cstr strlen getobjectinternaltype cstrf stopwatchcreate stopwatchstart stopwatchstop stopwatchelapsedseconds cnumber floor ceiling round abs getworldname getitemcountininventorybyname getheading getitemcountininventorybynamerx getheadingto actiontrygiveprofile vitae getfellowshipstatus getfellowshipname getfellowshipisopen getfellowshipisleader getfellowshipleaderid getfellowshipcanrecruit getfellowid getfellowshipcount getfellowshiplocked getfellowname getfellowshipisfull sin cos tan sqrt asin acos atan atan2 sinh cosh tanh vtsetmetastate getregexmatch echo chr ord wobjectgetid wobjectgethealth wobjectfindbyid wobjectgetintprop wobjectfindnearestbytemplatetype wobjectgetopencontainer testquestflag getquestktprogress isrefreshingquests getquestktrequired getqueststatus getisday getgamehour getgamehourname getisnight getgameday getgameticks getminutesuntilday getgamemonth getgamemonthname getminutesuntilnight getgameyear uisetvisible uiviewvisible uiviewexists getgvar touchgvar getpvar touchpvar setgvar cleargvar setpvar clearpvar testgvar clearallgvars testpvar clearallpvars dictgetitem dictcreate dicthaskey dictadditem dictkeys dictremovekey dictvalues dictclear dictsize dictcopy listgetitem listpop listcreate listcontains listremove listadd listindexof listremoveat listinsert listlastindexof listclear listcopy listcount listreverse
|
||||||
|
|
||||||
|
~~
|
||||||
|
~~ File auto-generated by metaf, a program created by Eskarina of Morningthaw/Coldeve.
|
||||||
|
~~ Get metaf here: https://github.com/JJEII/metaf/
|
||||||
|
~~
|
||||||
|
~~ All recognized structural designators in a NAV-ONLY file:
|
||||||
|
~~ NAV:
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV types:
|
||||||
|
~~ circular follow
|
||||||
|
~~ linear once
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV NODE types:
|
||||||
|
~~ flw vnd
|
||||||
|
~~ pnt ptl
|
||||||
|
~~ rcl tlk
|
||||||
|
~~ pau chk
|
||||||
|
~~ cht jmp
|
||||||
|
~~ prt (deprecated in VTank)
|
||||||
|
~~
|
||||||
|
|
||||||
|
~~
|
||||||
|
~~ REMEMBER THAT NAV-ONLY FILES MUST CONTAIN EXACTLY ONE NAV!
|
||||||
|
~~ }
|
||||||
|
|
||||||
|
NAV: nav0 once ~~ {
|
||||||
|
cht 59.1563772201538 -28.5892723083496 0.0500208298365275 {/ah}
|
||||||
|
pau 59.1563772201538 -28.5892723083496 0.0500208298365275 22000
|
||||||
|
cht 59.1563772201538 -28.5892723083496 0.0500208298365275 {/tell Time according to alex, ab}
|
||||||
|
pau 59.1563772201538 -28.5892723083496 0.0500208298365275 30000
|
||||||
|
cht 59.1563772201538 -28.5892723083496 0.0500208298365275 {/ub use Gateway}
|
||||||
|
pnt -87.9797062555949 -60.4362569173177 0.265733496348063
|
||||||
|
pnt -87.9643449147542 -60.3346110810836 0.230621592203776
|
||||||
|
pnt -87.9498847007751 -60.1938330491384 0.185985358556112
|
||||||
|
pnt -87.9534404754639 -60.1445933977763 0.175027068456014
|
||||||
|
pnt -87.9548230806986 -60.0900212287903 0.175027068456014
|
||||||
|
pnt -87.9550152778625 -60.0620187123617 0.175027068456014
|
||||||
|
pnt -87.9489116032918 -60.0324022928874 0.175027068456014
|
||||||
|
pnt -87.9241135279338 -59.9791566848755 0.175027068456014
|
||||||
|
~~ }
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
~~ {
|
||||||
|
~~ FOR AUTO-COMPLETION ASSISTANCE: testvar getvar setvar touchvar clearallvars clearvar getcharintprop getchardoubleprop getcharquadprop getcharboolprop getcharstringprop getisspellknown getcancastspell_hunt getcancastspell_buff getcharvital_base getcharvital_current getcharvital_buffedmax getcharskill_traininglevel getcharskill_base getcharskill_buffed getplayerlandcell getplayercoordinates coordinategetns coordinategetwe coordinategetz coordinatetostring coordinateparse coordinatedistancewithz coordinatedistanceflat wobjectgetphysicscoordinates wobjectgetname wobjectgetobjectclass wobjectgettemplatetype wobjectgetisdooropen wobjectfindnearestmonster wobjectfindnearestdoor wobjectfindnearestbyobjectclass wobjectfindininventorybytemplatetype wobjectfindininventorybyname wobjectfindininventorybynamerx wobjectgetselection wobjectgetplayer wobjectfindnearestbynameandobjectclass actiontryselect actiontryuseitem actiontryapplyitem actiontrygiveitem actiontryequipanywand actiontrycastbyid actiontrycastbyidontarget chatbox chatboxpaste statushud statushudcolored uigetcontrol uisetlabel isfalse istrue iif randint cstr strlen getobjectinternaltype cstrf stopwatchcreate stopwatchstart stopwatchstop stopwatchelapsedseconds cnumber floor ceiling round abs getworldname getitemcountininventorybyname getheading getitemcountininventorybynamerx getheadingto actiontrygiveprofile vitae getfellowshipstatus getfellowshipname getfellowshipisopen getfellowshipisleader getfellowshipleaderid getfellowshipcanrecruit getfellowid getfellowshipcount getfellowshiplocked getfellowname getfellowshipisfull sin cos tan sqrt asin acos atan atan2 sinh cosh tanh vtsetmetastate getregexmatch echo chr ord wobjectgetid wobjectgethealth wobjectfindbyid wobjectgetintprop wobjectfindnearestbytemplatetype wobjectgetopencontainer testquestflag getquestktprogress isrefreshingquests getquestktrequired getqueststatus getisday getgamehour getgamehourname getisnight getgameday getgameticks getminutesuntilday getgamemonth getgamemonthname getminutesuntilnight getgameyear uisetvisible uiviewvisible uiviewexists getgvar touchgvar getpvar touchpvar setgvar cleargvar setpvar clearpvar testgvar clearallgvars testpvar clearallpvars dictgetitem dictcreate dicthaskey dictadditem dictkeys dictremovekey dictvalues dictclear dictsize dictcopy listgetitem listpop listcreate listcontains listremove listadd listindexof listremoveat listinsert listlastindexof listclear listcopy listcount listreverse
|
||||||
|
|
||||||
|
~~
|
||||||
|
~~ File auto-generated by metaf, a program created by Eskarina of Morningthaw/Coldeve.
|
||||||
|
~~ Get metaf here: https://github.com/JJEII/metaf/
|
||||||
|
~~
|
||||||
|
~~ All recognized structural designators in a NAV-ONLY file:
|
||||||
|
~~ NAV:
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV types:
|
||||||
|
~~ circular follow
|
||||||
|
~~ linear once
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV NODE types:
|
||||||
|
~~ flw vnd
|
||||||
|
~~ pnt ptl
|
||||||
|
~~ rcl tlk
|
||||||
|
~~ pau chk
|
||||||
|
~~ cht jmp
|
||||||
|
~~ prt (deprecated in VTank)
|
||||||
|
~~
|
||||||
|
|
||||||
|
~~
|
||||||
|
~~ REMEMBER THAT NAV-ONLY FILES MUST CONTAIN EXACTLY ONE NAV!
|
||||||
|
~~ }
|
||||||
|
|
||||||
|
NAV: nav0 once ~~ {
|
||||||
|
pnt 48.3606951395671 25.9085505803426 0.231929063796997
|
||||||
|
pnt 48.3884344100952 25.5719860394796 0.239678192138672
|
||||||
|
pnt 48.3782159169515 25.5435764312744 0.225020837783813
|
||||||
|
pnt 48.3625609079997 25.4953356901805 0.225020837783813
|
||||||
|
pnt 48.3672090530396 25.4595303853353 0.225020837783813
|
||||||
|
pnt 48.3880900065104 25.4410440921783 0.225020837783813
|
||||||
|
chk 48.4019992192586 25.4926815191905 0.225020837783813
|
||||||
|
cht 48.3987060546875 25.4926479816437 0.225020837783813 {/ub give Blank Augmentation Gem to Brienne Carlus}
|
||||||
|
~~ }
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
~~ {
|
||||||
|
~~ FOR AUTO-COMPLETION ASSISTANCE: testvar getvar setvar touchvar clearallvars clearvar getcharintprop getchardoubleprop getcharquadprop getcharboolprop getcharstringprop getisspellknown getcancastspell_hunt getcancastspell_buff getcharvital_base getcharvital_current getcharvital_buffedmax getcharskill_traininglevel getcharskill_base getcharskill_buffed getplayerlandcell getplayercoordinates coordinategetns coordinategetwe coordinategetz coordinatetostring coordinateparse coordinatedistancewithz coordinatedistanceflat wobjectgetphysicscoordinates wobjectgetname wobjectgetobjectclass wobjectgettemplatetype wobjectgetisdooropen wobjectfindnearestmonster wobjectfindnearestdoor wobjectfindnearestbyobjectclass wobjectfindininventorybytemplatetype wobjectfindininventorybyname wobjectfindininventorybynamerx wobjectgetselection wobjectgetplayer wobjectfindnearestbynameandobjectclass actiontryselect actiontryuseitem actiontryapplyitem actiontrygiveitem actiontryequipanywand actiontrycastbyid actiontrycastbyidontarget chatbox chatboxpaste statushud statushudcolored uigetcontrol uisetlabel isfalse istrue iif randint cstr strlen getobjectinternaltype cstrf stopwatchcreate stopwatchstart stopwatchstop stopwatchelapsedseconds cnumber floor ceiling round abs getworldname getitemcountininventorybyname getheading getitemcountininventorybynamerx getheadingto actiontrygiveprofile vitae getfellowshipstatus getfellowshipname getfellowshipisopen getfellowshipisleader getfellowshipleaderid getfellowshipcanrecruit getfellowid getfellowshipcount getfellowshiplocked getfellowname getfellowshipisfull sin cos tan sqrt asin acos atan atan2 sinh cosh tanh vtsetmetastate getregexmatch echo chr ord wobjectgetid wobjectgethealth wobjectfindbyid wobjectgetintprop wobjectfindnearestbytemplatetype wobjectgetopencontainer testquestflag getquestktprogress isrefreshingquests getquestktrequired getqueststatus getisday getgamehour getgamehourname getisnight getgameday getgameticks getminutesuntilday getgamemonth getgamemonthname getminutesuntilnight getgameyear uisetvisible uiviewvisible uiviewexists getgvar touchgvar getpvar touchpvar setgvar cleargvar setpvar clearpvar testgvar clearallgvars testpvar clearallpvars dictgetitem dictcreate dicthaskey dictadditem dictkeys dictremovekey dictvalues dictclear dictsize dictcopy listgetitem listpop listcreate listcontains listremove listadd listindexof listremoveat listinsert listlastindexof listclear listcopy listcount listreverse
|
||||||
|
|
||||||
|
~~
|
||||||
|
~~ File auto-generated by metaf, a program created by Eskarina of Morningthaw/Coldeve.
|
||||||
|
~~ Get metaf here: https://github.com/JJEII/metaf/
|
||||||
|
~~
|
||||||
|
~~ All recognized structural designators in a NAV-ONLY file:
|
||||||
|
~~ NAV:
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV types:
|
||||||
|
~~ circular follow
|
||||||
|
~~ linear once
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV NODE types:
|
||||||
|
~~ flw vnd
|
||||||
|
~~ pnt ptl
|
||||||
|
~~ rcl tlk
|
||||||
|
~~ pau chk
|
||||||
|
~~ cht jmp
|
||||||
|
~~ prt (deprecated in VTank)
|
||||||
|
~~
|
||||||
|
|
||||||
|
~~
|
||||||
|
~~ REMEMBER THAT NAV-ONLY FILES MUST CONTAIN EXACTLY ONE NAV!
|
||||||
|
~~ }
|
||||||
|
|
||||||
|
NAV: nav0 once ~~ {
|
||||||
|
rcl -71.8128362019857 -46.2284189224243 2.08333212261399E-05 {Recall to the Singularity Caul}
|
||||||
|
pnt -94.7016312003136 -97.981881014506 0.367698383331299
|
||||||
|
ptl -71.8128362019857 -46.2284189224243 2.08333212261399E-05 -94.6987583319346 -98.0068958282471 0.371145516633987 14 {Obsidian Rim Portal}
|
||||||
|
pnt -65.5345490773519 -48.0928927580516 0.0821696281433106
|
||||||
|
pnt -66.2971390088399 -48.0919455369314 0.0824445406595866
|
||||||
|
pnt -68.2659324010213 -47.9679103851318 0.0901949644088745
|
||||||
|
pnt -68.9330024560293 -47.9617745717367 0.0741503397623698
|
||||||
|
pnt -68.9996176083883 -47.9868227322896 0.0811579465866089
|
||||||
|
pnt -69.0741902987162 -47.9962577819824 0.0902631839116414
|
||||||
|
pnt -69.1185588876406 -48.0084948221842 0.0896445353825887
|
||||||
|
pnt -69.1932870864868 -48.0247398058573 0.0864912668863932
|
||||||
|
pnt -69.2399808883667 -48.0406180699666 0.0826785326004028
|
||||||
|
pnt -69.3729923248291 -48.0507068951925 0.0753742853800456
|
||||||
|
pnt -69.502007261912 -48.0152909914653 0.0562341809272766
|
||||||
|
pnt -69.6805796305339 -47.9529766718547 0.00605392009019852
|
||||||
|
pnt -69.7465653419495 -47.9553860028585 0.000755888223648071
|
||||||
|
pnt -69.917982506752 -47.9998155911764 0.00417213241259257
|
||||||
|
pnt -70.4033851623535 -48.1885171890259 0.0103249351183573
|
||||||
|
pnt -71.328177467982 -48.5185417175293 2.08333212261399E-05
|
||||||
|
pnt -72.7273258845011 -47.8933919270833 0.0094555139541626
|
||||||
|
pnt -72.7809668223063 -47.776501083374 0.0289373199144999
|
||||||
|
pnt -72.7527559280395 -47.166915734609 0.135223134358724
|
||||||
|
pnt -72.7196399370829 -47.071635723114 0.0760513146718343
|
||||||
|
pnt -72.7042126337687 -47.0472258249919 0.0588250835736593
|
||||||
|
pnt -72.6919200261434 -47.0329411188761 0.0555859247843424
|
||||||
|
pnt -72.5977428436279 -46.9303064982096 0.0106916089852651
|
||||||
|
pnt -71.9506707509359 -46.338755830129 2.08333212261399E-05
|
||||||
|
pnt -71.8265404701233 -46.2414363861084 2.08333212261399E-05
|
||||||
|
ptl -71.8128362019857 -46.2284189224243 2.08333212261399E-05 -71.8083083470662 -46.2096791585286 -0.000262499845121056 14 {Empyrean Facility Upper Level}
|
||||||
|
~~ }
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
~~ {
|
||||||
|
~~ FOR AUTO-COMPLETION ASSISTANCE: testvar getvar setvar touchvar clearallvars clearvar getcharintprop getchardoubleprop getcharquadprop getcharboolprop getcharstringprop getisspellknown getcancastspell_hunt getcancastspell_buff getcharvital_base getcharvital_current getcharvital_buffedmax getcharskill_traininglevel getcharskill_base getcharskill_buffed getplayerlandcell getplayercoordinates coordinategetns coordinategetwe coordinategetz coordinatetostring coordinateparse coordinatedistancewithz coordinatedistanceflat wobjectgetphysicscoordinates wobjectgetname wobjectgetobjectclass wobjectgettemplatetype wobjectgetisdooropen wobjectfindnearestmonster wobjectfindnearestdoor wobjectfindnearestbyobjectclass wobjectfindininventorybytemplatetype wobjectfindininventorybyname wobjectfindininventorybynamerx wobjectgetselection wobjectgetplayer wobjectfindnearestbynameandobjectclass actiontryselect actiontryuseitem actiontryapplyitem actiontrygiveitem actiontryequipanywand actiontrycastbyid actiontrycastbyidontarget chatbox chatboxpaste statushud statushudcolored uigetcontrol uisetlabel isfalse istrue iif randint cstr strlen getobjectinternaltype cstrf stopwatchcreate stopwatchstart stopwatchstop stopwatchelapsedseconds cnumber floor ceiling round abs getworldname getitemcountininventorybyname getheading getitemcountininventorybynamerx getheadingto actiontrygiveprofile vitae getfellowshipstatus getfellowshipname getfellowshipisopen getfellowshipisleader getfellowshipleaderid getfellowshipcanrecruit getfellowid getfellowshipcount getfellowshiplocked getfellowname getfellowshipisfull sin cos tan sqrt asin acos atan atan2 sinh cosh tanh vtsetmetastate getregexmatch echo chr ord wobjectgetid wobjectgethealth wobjectfindbyid wobjectgetintprop wobjectfindnearestbytemplatetype wobjectgetopencontainer testquestflag getquestktprogress isrefreshingquests getquestktrequired getqueststatus getisday getgamehour getgamehourname getisnight getgameday getgameticks getminutesuntilday getgamemonth getgamemonthname getminutesuntilnight getgameyear uisetvisible uiviewvisible uiviewexists getgvar touchgvar getpvar touchpvar setgvar cleargvar setpvar clearpvar testgvar clearallgvars testpvar clearallpvars dictgetitem dictcreate dicthaskey dictadditem dictkeys dictremovekey dictvalues dictclear dictsize dictcopy listgetitem listpop listcreate listcontains listremove listadd listindexof listremoveat listinsert listlastindexof listclear listcopy listcount listreverse
|
||||||
|
|
||||||
|
~~
|
||||||
|
~~ File auto-generated by metaf, a program created by Eskarina of Morningthaw/Coldeve.
|
||||||
|
~~ Get metaf here: https://github.com/JJEII/metaf/
|
||||||
|
~~
|
||||||
|
~~ All recognized structural designators in a NAV-ONLY file:
|
||||||
|
~~ NAV:
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV types:
|
||||||
|
~~ circular follow
|
||||||
|
~~ linear once
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV NODE types:
|
||||||
|
~~ flw vnd
|
||||||
|
~~ pnt ptl
|
||||||
|
~~ rcl tlk
|
||||||
|
~~ pau chk
|
||||||
|
~~ cht jmp
|
||||||
|
~~ prt (deprecated in VTank)
|
||||||
|
~~
|
||||||
|
|
||||||
|
~~
|
||||||
|
~~ REMEMBER THAT NAV-ONLY FILES MUST CONTAIN EXACTLY ONE NAV!
|
||||||
|
~~ }
|
||||||
|
|
||||||
|
NAV: nav0 once ~~ {
|
||||||
|
cht 0.675179147720337 13.5718362808228 0.521687507629395 {/ah}
|
||||||
|
pau 0.675179147720337 13.5718362808228 0.521687507629395 15000
|
||||||
|
pnt 59.3342878341675 -28.5975779215495 0.0500208298365275
|
||||||
|
pnt 59.35896803538 -28.6749758720398 0.0500208298365275
|
||||||
|
pnt 59.37918065389 -28.7118357340495 0.0500208298365275
|
||||||
|
ptl 0.675179147720337 13.5718362808228 0.521687507629395 59.3936458587647 -28.7256083488464 0.0508250035345554 14 {Portal to Town Network}
|
||||||
|
pnt -101.617960484823 -96.6229596773783 2.08333134651184E-05
|
||||||
|
pnt -101.64374332428 -96.6113506952922 2.08333134651184E-05
|
||||||
|
pnt -101.677829106649 -96.6077419598897 2.08333134651184E-05
|
||||||
|
pnt -101.708999125163 -96.6393287976583 2.08333134651184E-05
|
||||||
|
pnt -101.720591862996 -96.644443766276 2.08333134651184E-05
|
||||||
|
pnt -101.724977588654 -96.6444894790649 2.08333134651184E-05
|
||||||
|
pnt -101.766201734543 -96.642304166158 2.08333134651184E-05
|
||||||
|
pnt -101.826050774256 -96.6380467732747 2.08333134651184E-05
|
||||||
|
pnt -101.873989931742 -96.6189005851746 2.08333134651184E-05
|
||||||
|
pnt -101.907649771372 -96.5925824801127 2.08333134651184E-05
|
||||||
|
pnt -101.907772219181 -96.5846062819163 2.08333134651184E-05
|
||||||
|
ptl 0.675179147720337 13.5718362808228 0.521687507629395 -101.90858749946 -96.5742208321889 -0.000262499845121056 14 {Portal to Zaikhal}
|
||||||
|
pnt 0.684857130050659 13.498526652654 0.516687520345052
|
||||||
|
pnt 0.648467270533244 13.6382585525513 0.516687520345052
|
||||||
|
pnt 0.663638496398926 13.6434263865153 0.516687520345052
|
||||||
|
tlk 0.675179147720337 13.5718362808228 0.521687507629395 0.675 13.6436941782633 0.516687512397766 37 {Jondor Torgren}
|
||||||
|
~~ }
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
~~ {
|
||||||
|
~~ FOR AUTO-COMPLETION ASSISTANCE: testvar getvar setvar touchvar clearallvars clearvar getcharintprop getchardoubleprop getcharquadprop getcharboolprop getcharstringprop getisspellknown getcancastspell_hunt getcancastspell_buff getcharvital_base getcharvital_current getcharvital_buffedmax getcharskill_traininglevel getcharskill_base getcharskill_buffed getplayerlandcell getplayercoordinates coordinategetns coordinategetwe coordinategetz coordinatetostring coordinateparse coordinatedistancewithz coordinatedistanceflat wobjectgetphysicscoordinates wobjectgetname wobjectgetobjectclass wobjectgettemplatetype wobjectgetisdooropen wobjectfindnearestmonster wobjectfindnearestdoor wobjectfindnearestbyobjectclass wobjectfindininventorybytemplatetype wobjectfindininventorybyname wobjectfindininventorybynamerx wobjectgetselection wobjectgetplayer wobjectfindnearestbynameandobjectclass actiontryselect actiontryuseitem actiontryapplyitem actiontrygiveitem actiontryequipanywand actiontrycastbyid actiontrycastbyidontarget chatbox chatboxpaste statushud statushudcolored uigetcontrol uisetlabel isfalse istrue iif randint cstr strlen getobjectinternaltype cstrf stopwatchcreate stopwatchstart stopwatchstop stopwatchelapsedseconds cnumber floor ceiling round abs getworldname getitemcountininventorybyname getheading getitemcountininventorybynamerx getheadingto actiontrygiveprofile vitae getfellowshipstatus getfellowshipname getfellowshipisopen getfellowshipisleader getfellowshipleaderid getfellowshipcanrecruit getfellowid getfellowshipcount getfellowshiplocked getfellowname getfellowshipisfull sin cos tan sqrt asin acos atan atan2 sinh cosh tanh vtsetmetastate getregexmatch echo chr ord wobjectgetid wobjectgethealth wobjectfindbyid wobjectgetintprop wobjectfindnearestbytemplatetype wobjectgetopencontainer testquestflag getquestktprogress isrefreshingquests getquestktrequired getqueststatus getisday getgamehour getgamehourname getisnight getgameday getgameticks getminutesuntilday getgamemonth getgamemonthname getminutesuntilnight getgameyear uisetvisible uiviewvisible uiviewexists getgvar touchgvar getpvar touchpvar setgvar cleargvar setpvar clearpvar testgvar clearallgvars testpvar clearallpvars dictgetitem dictcreate dicthaskey dictadditem dictkeys dictremovekey dictvalues dictclear dictsize dictcopy listgetitem listpop listcreate listcontains listremove listadd listindexof listremoveat listinsert listlastindexof listclear listcopy listcount listreverse
|
||||||
|
|
||||||
|
~~
|
||||||
|
~~ File auto-generated by metaf, a program created by Eskarina of Morningthaw/Coldeve.
|
||||||
|
~~ Get metaf here: https://github.com/JJEII/metaf/
|
||||||
|
~~
|
||||||
|
~~ All recognized structural designators in a NAV-ONLY file:
|
||||||
|
~~ NAV:
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV types:
|
||||||
|
~~ circular follow
|
||||||
|
~~ linear once
|
||||||
|
~~
|
||||||
|
~~ All recognized NAV NODE types:
|
||||||
|
~~ flw vnd
|
||||||
|
~~ pnt ptl
|
||||||
|
~~ rcl tlk
|
||||||
|
~~ pau chk
|
||||||
|
~~ cht jmp
|
||||||
|
~~ prt (deprecated in VTank)
|
||||||
|
~~
|
||||||
|
|
||||||
|
~~
|
||||||
|
~~ REMEMBER THAT NAV-ONLY FILES MUST CONTAIN EXACTLY ONE NAV!
|
||||||
|
~~ }
|
||||||
|
|
||||||
|
NAV: nav0 once ~~ {
|
||||||
|
cht 59.3306452433268 -28.5473848342896 0.0500208298365275 {/ub equip load idle.utl}
|
||||||
|
pau 59.3306452433268 -28.5473848342896 0.0500208298365275 10000
|
||||||
|
cht 59.3306452433268 -28.5473848342896 0.0500208298365275 {/ub equip load quest.utl}
|
||||||
|
pau 59.3306452433268 -28.5473848342896 0.0500208298365275 10000
|
||||||
|
rcl 59.3306452433268 -28.5473848342896 0.0500208298365275 {Primary Portal Recall}
|
||||||
|
pau 59.3306452433268 -28.5473848342896 0.0500208298365275 2000
|
||||||
|
pnt -101.643393707275 17.8825289090474 2.08333134651184E-05
|
||||||
|
cht 59.3306452433268 -28.5473848342896 0.0500208298365275 {/ub jumpsw 300}
|
||||||
|
pnt -101.592045052846 17.8805678685506 -0.0999791701634725
|
||||||
|
pnt -101.554651578267 17.8813594023387 -0.0999791701634725
|
||||||
|
pnt -101.533706283569 17.8833907604218 -0.0999791701634725
|
||||||
|
pnt -101.534181054433 17.8413492997487 -0.0999791701634725
|
||||||
|
pnt -101.48626130422 17.841304953893 -0.0999791701634725
|
||||||
|
pnt -101.490094470978 17.8813687324524 -0.0999791701634725
|
||||||
|
pnt -101.407286643982 17.884686867396 -0.124979170163473
|
||||||
|
pnt -101.441320260366 17.801603158315 -0.124979170163473
|
||||||
|
pnt -101.466675154368 17.7969429810842 -0.124979170163473
|
||||||
|
pnt -101.516472848256 17.8459821859996 -0.124979170163473
|
||||||
|
pnt -101.531043752035 17.8853654225667 -0.124979170163473
|
||||||
|
pnt -101.575432650248 17.8846518675486 -0.124979170163473
|
||||||
|
pnt -101.575820668538 17.7591938654582 -0.149979162216187
|
||||||
|
pnt -101.529988797506 17.7609985669454 -0.149979162216187
|
||||||
|
pnt -101.53424466451 17.8423960208893 -0.174979162216187
|
||||||
|
pnt -101.583576202393 17.8416724999746 -0.174979162216187
|
||||||
|
pnt -101.57072356542 17.8728516896566 -0.174979162216187
|
||||||
|
pnt -101.595848846436 17.8807149728139 -0.174979162216187
|
||||||
|
cht 59.3306452433268 -28.5473848342896 0.0500208298365275 {/ub jumpsw 300}
|
||||||
|
pnt -101.622127024333 17.8804373105367 -0.274979146321615
|
||||||
|
pnt -101.616229724884 17.9339917182922 -0.274979146321615
|
||||||
|
pnt -101.612587674459 17.9819168567657 -0.296296882629395
|
||||||
|
pnt -101.615892442067 18.0119011123975 -0.299979146321615
|
||||||
|
pnt -101.590025297801 18.0101968288422 -0.303131580352783
|
||||||
|
pnt -101.529243914286 18.0071381568909 -0.324979146321615
|
||||||
|
pnt -101.53581682841 17.9724334478378 -0.324979146321615
|
||||||
|
pnt -101.572706063588 17.9642690896988 -0.324979146321615
|
||||||
|
pnt -101.578296979268 17.9251905759176 -0.324979146321615
|
||||||
|
pnt -101.61803188324 17.9257889827093 -0.324979146321615
|
||||||
|
pnt -101.620800844828 17.9667853593826 -0.324979146321615
|
||||||
|
pnt -101.617472012838 18.0454295317332 -0.349979146321615
|
||||||
|
pnt -101.660903453827 18.0499082385407 -0.349979146321615
|
||||||
|
pnt -101.658096440633 17.9865849812826 -0.374979146321615
|
||||||
|
pnt -101.658587837219 17.9719900369644 -0.374979146321615
|
||||||
|
pnt -101.69858379364 17.9644167820613 -0.374979146321615
|
||||||
|
pnt -101.706780131658 17.9288707415263 -0.374979146321615
|
||||||
|
pnt -101.743702141444 17.921234814326 -0.374979146321615
|
||||||
|
pnt -101.775855763753 17.9226232131322 -0.374979146321615
|
||||||
|
pnt -101.784734662374 17.9344542185465 -0.374979146321615
|
||||||
|
pnt -101.78315264384 18.0072518110275 -0.349979146321615
|
||||||
|
pnt -101.86580046018 18.008648399512 -0.374979146321615
|
||||||
|
pnt -101.888455645243 18.0071597099304 -0.374979146321615
|
||||||
|
cht 59.3306452433268 -28.5473848342896 0.0500208298365275 {/ub jumpsw 300}
|
||||||
|
pnt -101.913387115796 18.0075169881185 -0.474979146321615
|
||||||
|
pnt -101.889330867926 18.0084470907847 -0.474979146321615
|
||||||
|
pnt -101.844996301333 17.9643644889196 -0.474979146321615
|
||||||
|
pnt -101.825047651927 17.9635365645091 -0.474979146321615
|
||||||
|
pnt -101.821242467562 17.9419899145762 -0.474979146321615
|
||||||
|
pnt -101.786994679769 17.9041363716125 -0.474979146321615
|
||||||
|
pnt -101.782102791468 17.8818953514099 -0.474979146321615
|
||||||
|
pnt -101.747181431452 17.8816454569499 -0.474979146321615
|
||||||
|
~~ }
|
||||||
1570
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/af/neftet.af
Normal file
1570
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/af/neftet.af
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,71 @@
|
||||||
|
UTL
|
||||||
|
1
|
||||||
|
1
|
||||||
|
peas
|
||||||
|
|
||||||
|
0;3;9999;15
|
||||||
|
6
|
||||||
|
true
|
||||||
|
30
|
||||||
|
255
|
||||||
|
255
|
||||||
|
255
|
||||||
|
10
|
||||||
|
0.1
|
||||||
|
None
|
||||||
|
SalvageCombine
|
||||||
|
277
|
||||||
|
1
|
||||||
|
1-6, 7-8, 9, 10
|
||||||
|
25
|
||||||
|
10
|
||||||
|
1-10
|
||||||
|
14
|
||||||
|
1-10
|
||||||
|
16
|
||||||
|
1-10
|
||||||
|
17
|
||||||
|
1-10
|
||||||
|
18
|
||||||
|
1-10
|
||||||
|
19
|
||||||
|
1-10
|
||||||
|
22
|
||||||
|
1-10
|
||||||
|
25
|
||||||
|
1-10
|
||||||
|
29
|
||||||
|
1-10
|
||||||
|
30
|
||||||
|
1-10
|
||||||
|
36
|
||||||
|
1-10
|
||||||
|
37
|
||||||
|
1-10
|
||||||
|
41
|
||||||
|
1-10
|
||||||
|
47
|
||||||
|
1-10
|
||||||
|
35
|
||||||
|
1-10
|
||||||
|
27
|
||||||
|
1-10
|
||||||
|
26
|
||||||
|
1-10
|
||||||
|
21
|
||||||
|
1-10
|
||||||
|
15
|
||||||
|
1-10
|
||||||
|
13
|
||||||
|
1-10
|
||||||
|
50
|
||||||
|
1-10
|
||||||
|
49
|
||||||
|
1-10
|
||||||
|
34
|
||||||
|
1-10
|
||||||
|
52
|
||||||
|
1-10
|
||||||
|
51
|
||||||
|
1-10
|
||||||
|
0
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
UTL
|
||||||
|
1
|
||||||
|
0
|
||||||
|
SalvageCombine
|
||||||
|
277
|
||||||
|
1
|
||||||
|
1-6, 7-8, 9, 10
|
||||||
|
25
|
||||||
|
10
|
||||||
|
1-10
|
||||||
|
14
|
||||||
|
1-10
|
||||||
|
16
|
||||||
|
1-10
|
||||||
|
17
|
||||||
|
1-10
|
||||||
|
18
|
||||||
|
1-10
|
||||||
|
19
|
||||||
|
1-10
|
||||||
|
22
|
||||||
|
1-10
|
||||||
|
25
|
||||||
|
1-10
|
||||||
|
29
|
||||||
|
1-10
|
||||||
|
30
|
||||||
|
1-10
|
||||||
|
36
|
||||||
|
1-10
|
||||||
|
37
|
||||||
|
1-10
|
||||||
|
41
|
||||||
|
1-10
|
||||||
|
47
|
||||||
|
1-10
|
||||||
|
35
|
||||||
|
1-10
|
||||||
|
27
|
||||||
|
1-10
|
||||||
|
26
|
||||||
|
1-10
|
||||||
|
21
|
||||||
|
1-10
|
||||||
|
15
|
||||||
|
1-10
|
||||||
|
13
|
||||||
|
1-10
|
||||||
|
50
|
||||||
|
1-10
|
||||||
|
49
|
||||||
|
1-10
|
||||||
|
34
|
||||||
|
1-10
|
||||||
|
52
|
||||||
|
1-10
|
||||||
|
51
|
||||||
|
1-10
|
||||||
|
0
|
||||||
931
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/loot-c.utl
Normal file
931
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/loot-c.utl
Normal file
|
|
@ -0,0 +1,931 @@
|
||||||
|
UTL
|
||||||
|
1
|
||||||
|
42
|
||||||
|
(H) 430 Insane
|
||||||
|
|
||||||
|
0;1;12;7;2008;2;13
|
||||||
|
9
|
||||||
|
44
|
||||||
|
159
|
||||||
|
3
|
||||||
|
1
|
||||||
|
17
|
||||||
|
109
|
||||||
|
1.17
|
||||||
|
1.17
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(H) Axe (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
3
|
||||||
|
353
|
||||||
|
9
|
||||||
|
44
|
||||||
|
159
|
||||||
|
15
|
||||||
|
84
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
33
|
||||||
|
Silifi|Lugian|War Axe|Battle
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(L) Axe (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
3
|
||||||
|
353
|
||||||
|
9
|
||||||
|
45
|
||||||
|
159
|
||||||
|
15
|
||||||
|
71
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
32
|
||||||
|
Dolabra|Ono|Hand|War Hammer
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(F) Axe (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
3
|
||||||
|
353
|
||||||
|
9
|
||||||
|
46
|
||||||
|
159
|
||||||
|
15
|
||||||
|
71
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
34
|
||||||
|
Hatchet|Shou-ono|Tungi|Hammer
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(H) Dagger (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
6
|
||||||
|
353
|
||||||
|
9
|
||||||
|
44
|
||||||
|
159
|
||||||
|
15
|
||||||
|
81
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
9
|
||||||
|
Dirk
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(H) Dagger MS (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
6
|
||||||
|
353
|
||||||
|
9
|
||||||
|
44
|
||||||
|
159
|
||||||
|
15
|
||||||
|
48
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
21
|
||||||
|
Stiletto|Jambiya
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(L) Dagger (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
6
|
||||||
|
353
|
||||||
|
9
|
||||||
|
45
|
||||||
|
159
|
||||||
|
15
|
||||||
|
68
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
12
|
||||||
|
Khanjar
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(L) Dagger MS (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
6
|
||||||
|
353
|
||||||
|
9
|
||||||
|
45
|
||||||
|
159
|
||||||
|
15
|
||||||
|
38
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
11
|
||||||
|
Dagger
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(F) Dagger (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
6
|
||||||
|
353
|
||||||
|
9
|
||||||
|
46
|
||||||
|
159
|
||||||
|
15
|
||||||
|
68
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
12
|
||||||
|
Poniard
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(F) Dagger MS (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
6
|
||||||
|
353
|
||||||
|
9
|
||||||
|
46
|
||||||
|
159
|
||||||
|
15
|
||||||
|
38
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
17
|
||||||
|
Knife|Lancet
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(H) Mace (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
4
|
||||||
|
353
|
||||||
|
9
|
||||||
|
44
|
||||||
|
159
|
||||||
|
15
|
||||||
|
79
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
24
|
||||||
|
Mazule|Mace|Morning
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(L) Mace (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
4
|
||||||
|
353
|
||||||
|
9
|
||||||
|
45
|
||||||
|
159
|
||||||
|
15
|
||||||
|
66
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
19
|
||||||
|
Club|Kasrullah
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(F) Mace (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
4
|
||||||
|
353
|
||||||
|
9
|
||||||
|
46
|
||||||
|
159
|
||||||
|
15
|
||||||
|
66
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
22
|
||||||
|
Board|Tofun|Dabus
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(H) Spear (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
5
|
||||||
|
353
|
||||||
|
9
|
||||||
|
44
|
||||||
|
159
|
||||||
|
15
|
||||||
|
82
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
28
|
||||||
|
Glaive|Trident|Partizan
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(L) Spear (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
5
|
||||||
|
353
|
||||||
|
9
|
||||||
|
45
|
||||||
|
159
|
||||||
|
15
|
||||||
|
70
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
15
|
||||||
|
Spear|Yari
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(F) Spear (OD +10 )
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
5
|
||||||
|
353
|
||||||
|
9
|
||||||
|
46
|
||||||
|
159
|
||||||
|
15
|
||||||
|
70
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
20
|
||||||
|
Naginata|Budiaq
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(H) Staff (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
7
|
||||||
|
353
|
||||||
|
9
|
||||||
|
44
|
||||||
|
159
|
||||||
|
15
|
||||||
|
79
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
16
|
||||||
|
Nabut|Stick
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(L) Staff (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
7
|
||||||
|
353
|
||||||
|
9
|
||||||
|
45
|
||||||
|
159
|
||||||
|
15
|
||||||
|
67
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
12
|
||||||
|
Quarter
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(F) Staff (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
7
|
||||||
|
353
|
||||||
|
9
|
||||||
|
46
|
||||||
|
159
|
||||||
|
15
|
||||||
|
67
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
15
|
||||||
|
Jo|Bastone
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(H) Sword (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
2
|
||||||
|
353
|
||||||
|
9
|
||||||
|
44
|
||||||
|
159
|
||||||
|
15
|
||||||
|
81
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
36
|
||||||
|
Takuba|Flamberge|Ken|Long|Tachi
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(H) Sword MS (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
2
|
||||||
|
353
|
||||||
|
9
|
||||||
|
44
|
||||||
|
159
|
||||||
|
15
|
||||||
|
48
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
13
|
||||||
|
Schlager
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(L) Sword (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
2
|
||||||
|
353
|
||||||
|
9
|
||||||
|
45
|
||||||
|
159
|
||||||
|
15
|
||||||
|
68
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
42
|
||||||
|
Dericost|Broad|Shamshir|Kaskara|Spada
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(L) Sword MS (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
2
|
||||||
|
353
|
||||||
|
9
|
||||||
|
45
|
||||||
|
159
|
||||||
|
15
|
||||||
|
37
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
9
|
||||||
|
Epee
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(F) Sword (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
2
|
||||||
|
353
|
||||||
|
9
|
||||||
|
46
|
||||||
|
159
|
||||||
|
15
|
||||||
|
68
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
36
|
||||||
|
Scimitar|Yaoji|Short|Sabra|Simi
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(F) Sword MS (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
2
|
||||||
|
353
|
||||||
|
9
|
||||||
|
46
|
||||||
|
159
|
||||||
|
15
|
||||||
|
38
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
11
|
||||||
|
Rapier
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(H) UA (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
1
|
||||||
|
353
|
||||||
|
9
|
||||||
|
44
|
||||||
|
159
|
||||||
|
15
|
||||||
|
69
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
18
|
||||||
|
Nekode|Cestus
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(L) UA (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
1
|
||||||
|
353
|
||||||
|
9
|
||||||
|
45
|
||||||
|
159
|
||||||
|
15
|
||||||
|
58
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
19
|
||||||
|
Katar|Knuckles
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(F) UA (OD +10)
|
||||||
|
|
||||||
|
0;1;12;12;2003;7;1;2;13
|
||||||
|
8
|
||||||
|
1
|
||||||
|
353
|
||||||
|
9
|
||||||
|
46
|
||||||
|
159
|
||||||
|
15
|
||||||
|
58
|
||||||
|
218103842
|
||||||
|
3
|
||||||
|
1
|
||||||
|
15
|
||||||
|
Claw|Wraps
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(2H) Cleaving (OD +10)
|
||||||
|
|
||||||
|
0;1;7;2003;12;1;2;13
|
||||||
|
3
|
||||||
|
1
|
||||||
|
15
|
||||||
|
55
|
||||||
|
218103842
|
||||||
|
9
|
||||||
|
41
|
||||||
|
159
|
||||||
|
75
|
||||||
|
Nodachi|Shashqa|Spadone|Greataxe|Quadrelle|Khanda-handled|Tetsubo|Star
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(2H) Cleaving (OD +9)
|
||||||
|
|
||||||
|
0;1;7;2003;12;1;2;13
|
||||||
|
3
|
||||||
|
1
|
||||||
|
15
|
||||||
|
54
|
||||||
|
218103842
|
||||||
|
9
|
||||||
|
41
|
||||||
|
159
|
||||||
|
75
|
||||||
|
Nodachi|Shashqa|Spadone|Greataxe|Quadrelle|Khanda-handled|Tetsubo|Star
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(2H) Cleaving (OD +8)
|
||||||
|
|
||||||
|
0;1;7;2003;12;1;2;13
|
||||||
|
3
|
||||||
|
1
|
||||||
|
15
|
||||||
|
53
|
||||||
|
218103842
|
||||||
|
9
|
||||||
|
41
|
||||||
|
159
|
||||||
|
75
|
||||||
|
Nodachi|Shashqa|Spadone|Greataxe|Quadrelle|Khanda-handled|Tetsubo|Star
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(2H) Spear (OD +10)
|
||||||
|
|
||||||
|
0;1;7;2003;12;1;2;13
|
||||||
|
3
|
||||||
|
1
|
||||||
|
15
|
||||||
|
58
|
||||||
|
218103842
|
||||||
|
9
|
||||||
|
41
|
||||||
|
159
|
||||||
|
33
|
||||||
|
Assagai|Pike|Magari|Corsesca
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(M) Bow (OD +10)
|
||||||
|
|
||||||
|
0;1;12;7;2001;12;2;13
|
||||||
|
8
|
||||||
|
8
|
||||||
|
353
|
||||||
|
3
|
||||||
|
9
|
||||||
|
6
|
||||||
|
78.6
|
||||||
|
9
|
||||||
|
47
|
||||||
|
159
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(M) Xbow (OD +10)
|
||||||
|
|
||||||
|
0;1;12;7;2001;12;2;13
|
||||||
|
8
|
||||||
|
9
|
||||||
|
353
|
||||||
|
3
|
||||||
|
9
|
||||||
|
4
|
||||||
|
87
|
||||||
|
9
|
||||||
|
47
|
||||||
|
159
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(M) TW (OD +10)
|
||||||
|
|
||||||
|
0;1;12;7;2001;12;2;13
|
||||||
|
9
|
||||||
|
10
|
||||||
|
353
|
||||||
|
3
|
||||||
|
9
|
||||||
|
6
|
||||||
|
85.3
|
||||||
|
9
|
||||||
|
47
|
||||||
|
159
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(W) No Wield (29MD)
|
||||||
|
|
||||||
|
0;1;7;2005;13;13;13;1;13;2;13
|
||||||
|
4
|
||||||
|
31
|
||||||
|
10
|
||||||
|
1.29
|
||||||
|
29
|
||||||
|
10
|
||||||
|
375
|
||||||
|
160
|
||||||
|
10
|
||||||
|
355
|
||||||
|
160
|
||||||
|
10
|
||||||
|
330
|
||||||
|
160
|
||||||
|
27
|
||||||
|
Orb|Wand|Sceptre|Staff
|
||||||
|
1
|
||||||
|
10
|
||||||
|
385
|
||||||
|
160
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(W) No Wield (28MD)
|
||||||
|
|
||||||
|
0;1;7;2005;13;13;13;1;13;2;13
|
||||||
|
4
|
||||||
|
31
|
||||||
|
10
|
||||||
|
1.28
|
||||||
|
29
|
||||||
|
10
|
||||||
|
375
|
||||||
|
160
|
||||||
|
10
|
||||||
|
355
|
||||||
|
160
|
||||||
|
10
|
||||||
|
330
|
||||||
|
160
|
||||||
|
27
|
||||||
|
Orb|Wand|Sceptre|Staff
|
||||||
|
1
|
||||||
|
10
|
||||||
|
385
|
||||||
|
160
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(W) Void (OD +7)
|
||||||
|
|
||||||
|
0;1;7;2005;1;2;13
|
||||||
|
4
|
||||||
|
31
|
||||||
|
11
|
||||||
|
1.25
|
||||||
|
152
|
||||||
|
11
|
||||||
|
Nether
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(W) Void (OD +6)
|
||||||
|
|
||||||
|
0;1;7;2005;1;2;13
|
||||||
|
4
|
||||||
|
31
|
||||||
|
11
|
||||||
|
1.24
|
||||||
|
152
|
||||||
|
11
|
||||||
|
Nether
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(W) Void (20md + 4Mgcd)
|
||||||
|
|
||||||
|
0;1;7;5;1;2005;2;13
|
||||||
|
4
|
||||||
|
31
|
||||||
|
11
|
||||||
|
1.04
|
||||||
|
150
|
||||||
|
11
|
||||||
|
Nether
|
||||||
|
1
|
||||||
|
9
|
||||||
|
1.2
|
||||||
|
29
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(W) Void (29MD)
|
||||||
|
|
||||||
|
0;1;7;1;2005;2;13
|
||||||
|
4
|
||||||
|
31
|
||||||
|
11
|
||||||
|
Nether
|
||||||
|
1
|
||||||
|
10
|
||||||
|
1.29
|
||||||
|
29
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
(W) War (OD +7)
|
||||||
|
|
||||||
|
0;1;7;2005;1;2;13
|
||||||
|
4
|
||||||
|
31
|
||||||
|
11
|
||||||
|
1.25
|
||||||
|
152
|
||||||
|
24
|
||||||
|
Baton|Sceptre|Staff
|
||||||
|
1
|
||||||
|
8
|
||||||
|
0
|
||||||
|
171
|
||||||
|
18
|
||||||
|
23308
|
||||||
|
218103849
|
||||||
|
SalvageCombine
|
||||||
|
11
|
||||||
|
1
|
||||||
|
|
||||||
|
0
|
||||||
|
0
|
||||||
121
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/loot-d.utl
Normal file
121
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/loot-d.utl
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
UTL
|
||||||
|
1
|
||||||
|
90
|
||||||
|
***** Trophies *****
|
||||||
|
|
||||||
|
0;1;9999;1
|
||||||
|
6
|
||||||
|
true
|
||||||
|
24
|
||||||
|
leave this disabled
|
||||||
|
1
|
||||||
|
|
||||||
|
|
||||||
|
0;1;9999
|
||||||
|
6
|
||||||
|
true
|
||||||
|
** Aetheria **
|
||||||
|
|
||||||
|
0;1;1
|
||||||
|
16
|
||||||
|
do not edit
|
||||||
|
1
|
||||||
|
(T) Aetheria (5)
|
||||||
|
|
||||||
|
0;1;1;12
|
||||||
|
13
|
||||||
|
Aetheria
|
||||||
|
1
|
||||||
|
18
|
||||||
|
27704
|
||||||
|
218103849
|
||||||
|
(T) Aetheria (4+)
|
||||||
|
|
||||||
|
0;1;1;3
|
||||||
|
13
|
||||||
|
Aetheria
|
||||||
|
1
|
||||||
|
18
|
||||||
|
27703
|
||||||
|
218103849
|
||||||
|
(T) Aetheria (3+)
|
||||||
|
|
||||||
|
0;1;1;3;9999
|
||||||
|
13
|
||||||
|
Aetheria
|
||||||
|
1
|
||||||
|
18
|
||||||
|
27702
|
||||||
|
218103849
|
||||||
|
6
|
||||||
|
true
|
||||||
|
(T) Aetheria (All)
|
||||||
|
|
||||||
|
0;1;1;9999
|
||||||
|
23
|
||||||
|
Coalesced Aetheria
|
||||||
|
1
|
||||||
|
6
|
||||||
|
true
|
||||||
|
(T) Aetheria (Red only)
|
||||||
|
|
||||||
|
0;1;1;12;9999
|
||||||
|
23
|
||||||
|
Coalesced Aetheria
|
||||||
|
1
|
||||||
|
18
|
||||||
|
42636
|
||||||
|
218103808
|
||||||
|
6
|
||||||
|
true
|
||||||
|
|
||||||
|
|
||||||
|
0;1;9999
|
||||||
|
6
|
||||||
|
true
|
||||||
|
** Armor **
|
||||||
|
|
||||||
|
0;1;1
|
||||||
|
16
|
||||||
|
do not edit
|
||||||
|
1
|
||||||
|
(T) Rynthid Masks
|
||||||
|
|
||||||
|
0;1;1;7
|
||||||
|
12
|
||||||
|
Rynthid
|
||||||
|
1
|
||||||
|
3
|
||||||
|
2
|
||||||
|
|
||||||
|
|
||||||
|
0;1;9999
|
||||||
|
6
|
||||||
|
true
|
||||||
|
** Consumables **
|
||||||
|
|
||||||
|
0;1;1
|
||||||
|
16
|
||||||
|
do not edit
|
||||||
|
1
|
||||||
|
(T) Stamina Philtre
|
||||||
|
|
||||||
|
0;1;1
|
||||||
|
20
|
||||||
|
Stamina Philtre
|
||||||
|
1
|
||||||
|
(T) Health Philtre
|
||||||
|
|
||||||
|
0;1;1
|
||||||
|
19
|
||||||
|
Health Philtre
|
||||||
|
1
|
||||||
|
(T) Mana Philtre
|
||||||
|
|
||||||
|
0;1;1;9999
|
||||||
|
17
|
||||||
|
Mana Philtre
|
||||||
|
1
|
||||||
|
6
|
||||||
|
true
|
||||||
|
(T) Stamina Elixir
|
||||||
9187
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/met/aphus.met
Normal file
9187
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/met/aphus.met
Normal file
File diff suppressed because it is too large
Load diff
2418
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/met/augments.met
Normal file
2418
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/met/augments.met
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,291 @@
|
||||||
|
1
|
||||||
|
CondAct
|
||||||
|
5
|
||||||
|
CType
|
||||||
|
AType
|
||||||
|
CData
|
||||||
|
AData
|
||||||
|
State
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
11
|
||||||
|
i
|
||||||
|
20
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
mp_primary
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/mp
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
i
|
||||||
|
20
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set enablenav true
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
turn_in_jaw
|
||||||
|
s
|
||||||
|
jaw_hunt
|
||||||
|
i
|
||||||
|
12
|
||||||
|
i
|
||||||
|
3
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
2
|
||||||
|
s
|
||||||
|
n
|
||||||
|
s
|
||||||
|
Insatiable Eater Jaw
|
||||||
|
s
|
||||||
|
c
|
||||||
|
i
|
||||||
|
1
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt nav load to_fiun
|
||||||
|
s
|
||||||
|
jaw_hunt
|
||||||
|
i
|
||||||
|
7
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
4
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt nav load jaw_1_hunt
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set enablecombat true
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set enablelooting true
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
jaw_hunt
|
||||||
|
s
|
||||||
|
jaw_run
|
||||||
|
i
|
||||||
|
28
|
||||||
|
i
|
||||||
|
3
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
2
|
||||||
|
s
|
||||||
|
p
|
||||||
|
s
|
||||||
|
You have solved this quest too recently!
|
||||||
|
s
|
||||||
|
c
|
||||||
|
s
|
||||||
|
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt nav load recallls
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set enablelooting false
|
||||||
|
s
|
||||||
|
kill_bella
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
7
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt nav load mp_primary
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set enablebuffing true
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set enablenav true
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set enablecombat true
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set enablelooting false
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set NavPriorityBoost false
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set navclosestoprange 0.008
|
||||||
|
s
|
||||||
|
mp_primary
|
||||||
|
i
|
||||||
|
20
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt nav load jaw_1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
jaw_run
|
||||||
|
s
|
||||||
|
mp_primary
|
||||||
|
i
|
||||||
|
20
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt nav load to_bella
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
kill_bella
|
||||||
|
s
|
||||||
|
mp_secondary
|
||||||
|
i
|
||||||
|
20
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt nav load mp_secondary
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
mp_secondary
|
||||||
|
s
|
||||||
|
mp_trans
|
||||||
|
i
|
||||||
|
4
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
^Fiun Rehlyun.*
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/mp
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
mp_trans
|
||||||
|
s
|
||||||
|
turn_in_jaw
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,910 @@
|
||||||
|
1
|
||||||
|
CondAct
|
||||||
|
5
|
||||||
|
CType
|
||||||
|
AType
|
||||||
|
CData
|
||||||
|
AData
|
||||||
|
State
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
33
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
2
|
||||||
|
i
|
||||||
|
7
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
setvar[mssortstring,`CV,AS,EQ,TR-,OC,IC,AL-,EP,MX-`]
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
SortManaStones
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set source player
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set dest pack 7
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set ocfilter manastone
|
||||||
|
i
|
||||||
|
8
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
`/ms set flags `+getvar[mssortstring]
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms start
|
||||||
|
s
|
||||||
|
SortManaStones
|
||||||
|
i
|
||||||
|
4
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
^You think\, \"done sorting\!\"$
|
||||||
|
s
|
||||||
|
SortHealingKits
|
||||||
|
s
|
||||||
|
SortManaStones
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set source player
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set dest pack 7
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set ocfilter healingkit
|
||||||
|
i
|
||||||
|
8
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
`/ms set flags `+getvar[mssortstring]
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms start
|
||||||
|
s
|
||||||
|
SortHealingKits
|
||||||
|
i
|
||||||
|
4
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
^You think\, \"done sorting\!\"$
|
||||||
|
s
|
||||||
|
SortMisc
|
||||||
|
s
|
||||||
|
SortHealingKits
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set source player
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set dest pack 2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set ocfilter misc
|
||||||
|
i
|
||||||
|
8
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
`/ms set flags `+getvar[mssortstring]
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms start
|
||||||
|
s
|
||||||
|
SortMisc
|
||||||
|
i
|
||||||
|
4
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
^You think\, \"done sorting\!\"$
|
||||||
|
s
|
||||||
|
SortGems
|
||||||
|
s
|
||||||
|
SortMisc
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set source player
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set dest pack 3
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set ocfilter gem
|
||||||
|
i
|
||||||
|
8
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
`/ms set flags `+getvar[mssortstring]
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms start
|
||||||
|
s
|
||||||
|
SortGems
|
||||||
|
i
|
||||||
|
4
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
^You think\, \"done sorting\!\"$
|
||||||
|
s
|
||||||
|
SortWeapons
|
||||||
|
s
|
||||||
|
SortGems
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set source player
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set dest pack 1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set ocfilter meleeweapon
|
||||||
|
i
|
||||||
|
8
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
`/ms set flags `+getvar[mssortstring]
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms start
|
||||||
|
s
|
||||||
|
SortWeapons
|
||||||
|
i
|
||||||
|
4
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
^You think\, \"done sorting\!\"$
|
||||||
|
s
|
||||||
|
SortWands
|
||||||
|
s
|
||||||
|
SortWeapons
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set source player
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set dest pack 1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set ocfilter wand
|
||||||
|
i
|
||||||
|
8
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
`/ms set flags `+getvar[mssortstring]
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms start
|
||||||
|
s
|
||||||
|
SortWands
|
||||||
|
i
|
||||||
|
4
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
^You think\, \"done sorting\!\"$
|
||||||
|
s
|
||||||
|
SortSpellComps
|
||||||
|
s
|
||||||
|
SortWands
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set source player
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set dest pack 8
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set ocfilter spellcomp
|
||||||
|
i
|
||||||
|
8
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
`/ms set flags `+getvar[mssortstring]
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms start
|
||||||
|
s
|
||||||
|
SortSpellComps
|
||||||
|
i
|
||||||
|
4
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
^You think\, \"done sorting\!\"$
|
||||||
|
s
|
||||||
|
SortInventory
|
||||||
|
s
|
||||||
|
SortSpellComps
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set source player
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set dest player
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms clear ocfilter
|
||||||
|
i
|
||||||
|
8
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
`/ms set flags `+getvar[mssortstring]
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms start
|
||||||
|
s
|
||||||
|
SortInventory
|
||||||
|
i
|
||||||
|
4
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
^You think\, \"done sorting\!\"$
|
||||||
|
s
|
||||||
|
SortPack1
|
||||||
|
s
|
||||||
|
SortInventory
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set source pack 1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set dest pack 1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms clear ocfilter
|
||||||
|
i
|
||||||
|
8
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
`/ms set flags `+getvar[mssortstring]
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms start
|
||||||
|
s
|
||||||
|
SortPack1
|
||||||
|
i
|
||||||
|
4
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
^You think\, \"done sorting\!\"$
|
||||||
|
s
|
||||||
|
SortPack2
|
||||||
|
s
|
||||||
|
SortPack1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set source pack 2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set dest pack 2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms clear ocfilter
|
||||||
|
i
|
||||||
|
8
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
`/ms set flags `+getvar[mssortstring]
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms start
|
||||||
|
s
|
||||||
|
SortPack2
|
||||||
|
i
|
||||||
|
4
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
^You think\, \"done sorting\!\"$
|
||||||
|
s
|
||||||
|
SortPack3
|
||||||
|
s
|
||||||
|
SortPack2
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set source pack 3
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set dest pack 3
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms clear ocfilter
|
||||||
|
i
|
||||||
|
8
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
`/ms set flags `+getvar[mssortstring]
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms start
|
||||||
|
s
|
||||||
|
SortPack3
|
||||||
|
i
|
||||||
|
4
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
^You think\, \"done sorting\!\"$
|
||||||
|
s
|
||||||
|
SortPack4
|
||||||
|
s
|
||||||
|
SortPack3
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set source pack 4
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set dest pack 4
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms clear ocfilter
|
||||||
|
i
|
||||||
|
8
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
`/ms set flags `+getvar[mssortstring]
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms start
|
||||||
|
s
|
||||||
|
SortPack4
|
||||||
|
i
|
||||||
|
4
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
^You think\, \"done sorting\!\"$
|
||||||
|
s
|
||||||
|
SortPack5
|
||||||
|
s
|
||||||
|
SortPack4
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set source pack 5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set dest pack 5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms clear ocfilter
|
||||||
|
i
|
||||||
|
8
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
`/ms set flags `+getvar[mssortstring]
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms start
|
||||||
|
s
|
||||||
|
SortPack5
|
||||||
|
i
|
||||||
|
4
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
^You think\, \"done sorting\!\"$
|
||||||
|
s
|
||||||
|
SortPack6
|
||||||
|
s
|
||||||
|
SortPack5
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set source pack 6
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set dest pack 6
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms clear ocfilter
|
||||||
|
i
|
||||||
|
8
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
`/ms set flags `+getvar[mssortstring]
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms start
|
||||||
|
s
|
||||||
|
SortPack6
|
||||||
|
i
|
||||||
|
4
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
^You think\, \"done sorting\!\"$
|
||||||
|
s
|
||||||
|
SortPack7
|
||||||
|
s
|
||||||
|
SortPack6
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set source pack 7
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set dest pack 7
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms clear ocfilter
|
||||||
|
i
|
||||||
|
8
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
`/ms set flags `+getvar[mssortstring]
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms start
|
||||||
|
s
|
||||||
|
SortPack7
|
||||||
|
i
|
||||||
|
4
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
^You think\, \"done sorting\!\"$
|
||||||
|
s
|
||||||
|
SortPack8
|
||||||
|
s
|
||||||
|
SortPack7
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set source pack 8
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms set dest pack 8
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms clear ocfilter
|
||||||
|
i
|
||||||
|
8
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
`/ms set flags `+getvar[mssortstring]
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ms start
|
||||||
|
s
|
||||||
|
SortPack8
|
||||||
|
i
|
||||||
|
4
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
^You think\, \"done sorting\!\"$
|
||||||
|
s
|
||||||
|
AllDone
|
||||||
|
s
|
||||||
|
SortPack8
|
||||||
|
|
@ -0,0 +1,554 @@
|
||||||
|
1
|
||||||
|
CondAct
|
||||||
|
5
|
||||||
|
CType
|
||||||
|
AType
|
||||||
|
CData
|
||||||
|
AData
|
||||||
|
State
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
42
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /vt setmetastate just_run_back
|
||||||
|
s
|
||||||
|
all_run_back
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ub bc /ub use Luminous Crystal of Surging Strength
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
s
|
||||||
|
strength
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /ub closestportal
|
||||||
|
s
|
||||||
|
closestportal
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
s
|
||||||
|
closestportal
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/vt nav load death_pause
|
||||||
|
s
|
||||||
|
death
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/vt opt set enablenav true
|
||||||
|
s
|
||||||
|
death
|
||||||
|
i
|
||||||
|
7
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
run_back
|
||||||
|
s
|
||||||
|
death
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
i
|
||||||
|
8
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
death
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ub bc /ub use Luminous Crystal of Towering Defense
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
s
|
||||||
|
defence
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ub bc /ub use Black Market Gem of Dispelling
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
s
|
||||||
|
dispell
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
2
|
||||||
|
i
|
||||||
|
8
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
\/ub bc \/ub follow +wobjectgetname[wobjectgetplayer[]]
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/ub bc /vt opt set enablenav true
|
||||||
|
s
|
||||||
|
follow
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
s
|
||||||
|
follow
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /ub jumpsw 400
|
||||||
|
s
|
||||||
|
jump
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
s
|
||||||
|
jump
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt nav load gauntlet_just_run
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set enablenav true
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set enablebuffing true
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set enablemeta true
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set enablecombat true
|
||||||
|
s
|
||||||
|
just_run_back
|
||||||
|
i
|
||||||
|
7
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
s
|
||||||
|
just_run_back
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /vt meta load gauntlet_follower
|
||||||
|
s
|
||||||
|
load_follow
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
s
|
||||||
|
load_follow
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /vt opt set enablenav false
|
||||||
|
s
|
||||||
|
nav_false
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
s
|
||||||
|
nav_false
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /vt opt set enablenav true
|
||||||
|
s
|
||||||
|
nav_true
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
s
|
||||||
|
nav_true
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
K
|
||||||
|
V
|
||||||
|
n
|
||||||
|
n
|
||||||
|
5
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt nav load gauntlet_run
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set enablenav true
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set enablebuffing true
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set enablemeta true
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
/vt opt set enablecombat true
|
||||||
|
s
|
||||||
|
run_back
|
||||||
|
i
|
||||||
|
7
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
s
|
||||||
|
run_back
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
8
|
||||||
|
i
|
||||||
|
0
|
||||||
|
TABLE
|
||||||
|
2
|
||||||
|
k
|
||||||
|
v
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
e
|
||||||
|
s
|
||||||
|
\/ub bc \/ub follow +wobjectgetname[wobjectgetplayer[]]
|
||||||
|
s
|
||||||
|
setup
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /vt opt set enablelooting false
|
||||||
|
s
|
||||||
|
setup
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /vt opt set enablenav true
|
||||||
|
s
|
||||||
|
setup
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /vt opt set enablebuffing true
|
||||||
|
s
|
||||||
|
setup
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /vt opt set enablecombat true
|
||||||
|
s
|
||||||
|
setup
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /vt opt set attackdistance 0.125
|
||||||
|
s
|
||||||
|
setup
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /vt opt set navpriorityboost true
|
||||||
|
s
|
||||||
|
setup
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /vt opt set buffprofile_prots 2
|
||||||
|
s
|
||||||
|
setup
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /vt opt set buffprofile_banes 2
|
||||||
|
s
|
||||||
|
setup
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /vt forcebuff
|
||||||
|
s
|
||||||
|
setup
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /vt start
|
||||||
|
s
|
||||||
|
setup
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /vt opt set enablemeta true
|
||||||
|
s
|
||||||
|
setup
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
s
|
||||||
|
setup
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /vt opt set SummonPets false
|
||||||
|
s
|
||||||
|
summon_false
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
s
|
||||||
|
summon_false
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
/ub bc /vt opt set SummonPets true
|
||||||
|
s
|
||||||
|
summon_true
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
Default
|
||||||
|
s
|
||||||
|
summon_true
|
||||||
File diff suppressed because it is too large
Load diff
13805
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/met/neftet.met
Normal file
13805
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/met/neftet.met
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,73 @@
|
||||||
|
uTank2 NAV 1.2
|
||||||
|
4
|
||||||
|
13
|
||||||
|
4
|
||||||
|
59.1563772201538
|
||||||
|
-28.5892723083496
|
||||||
|
0.0500208298365275
|
||||||
|
0
|
||||||
|
/ah
|
||||||
|
3
|
||||||
|
59.1563772201538
|
||||||
|
-28.5892723083496
|
||||||
|
0.0500208298365275
|
||||||
|
0
|
||||||
|
22000
|
||||||
|
4
|
||||||
|
59.1563772201538
|
||||||
|
-28.5892723083496
|
||||||
|
0.0500208298365275
|
||||||
|
0
|
||||||
|
/tell Time according to alex, ab
|
||||||
|
3
|
||||||
|
59.1563772201538
|
||||||
|
-28.5892723083496
|
||||||
|
0.0500208298365275
|
||||||
|
0
|
||||||
|
30000
|
||||||
|
4
|
||||||
|
59.1563772201538
|
||||||
|
-28.5892723083496
|
||||||
|
0.0500208298365275
|
||||||
|
0
|
||||||
|
/ub use Gateway
|
||||||
|
0
|
||||||
|
-87.9797062555949
|
||||||
|
-60.4362569173177
|
||||||
|
0.265733496348063
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-87.9643449147542
|
||||||
|
-60.3346110810836
|
||||||
|
0.230621592203776
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-87.9498847007751
|
||||||
|
-60.1938330491384
|
||||||
|
0.185985358556112
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-87.9534404754639
|
||||||
|
-60.1445933977763
|
||||||
|
0.175027068456014
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-87.9548230806986
|
||||||
|
-60.0900212287903
|
||||||
|
0.175027068456014
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-87.9550152778625
|
||||||
|
-60.0620187123617
|
||||||
|
0.175027068456014
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-87.9489116032918
|
||||||
|
-60.0324022928874
|
||||||
|
0.175027068456014
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-87.9241135279338
|
||||||
|
-59.9791566848755
|
||||||
|
0.175027068456014
|
||||||
|
0
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
uTank2 NAV 1.2
|
||||||
|
4
|
||||||
|
8
|
||||||
|
0
|
||||||
|
48.3606951395671
|
||||||
|
25.9085505803426
|
||||||
|
0.231929063796997
|
||||||
|
0
|
||||||
|
0
|
||||||
|
48.3884344100952
|
||||||
|
25.5719860394796
|
||||||
|
0.239678192138672
|
||||||
|
0
|
||||||
|
0
|
||||||
|
48.3782159169515
|
||||||
|
25.5435764312744
|
||||||
|
0.225020837783813
|
||||||
|
0
|
||||||
|
0
|
||||||
|
48.3625609079997
|
||||||
|
25.4953356901805
|
||||||
|
0.225020837783813
|
||||||
|
0
|
||||||
|
0
|
||||||
|
48.3672090530396
|
||||||
|
25.4595303853353
|
||||||
|
0.225020837783813
|
||||||
|
0
|
||||||
|
0
|
||||||
|
48.3880900065104
|
||||||
|
25.4410440921783
|
||||||
|
0.225020837783813
|
||||||
|
0
|
||||||
|
8
|
||||||
|
48.4019992192586
|
||||||
|
25.4926815191905
|
||||||
|
0.225020837783813
|
||||||
|
0
|
||||||
|
4
|
||||||
|
48.3987060546875
|
||||||
|
25.4926479816437
|
||||||
|
0.225020837783813
|
||||||
|
0
|
||||||
|
/ub give Blank Augmentation Gem to Brienne Carlus
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
uTank2 NAV 1.2
|
||||||
|
4
|
||||||
|
29
|
||||||
|
2
|
||||||
|
-71.8128362019857
|
||||||
|
-46.2284189224243
|
||||||
|
2.08333212261399E-05
|
||||||
|
0
|
||||||
|
2943
|
||||||
|
0
|
||||||
|
-94.7016312003136
|
||||||
|
-97.981881014506
|
||||||
|
0.367698383331299
|
||||||
|
0
|
||||||
|
6
|
||||||
|
-71.8128362019857
|
||||||
|
-46.2284189224243
|
||||||
|
2.08333212261399E-05
|
||||||
|
0
|
||||||
|
Obsidian Rim Portal
|
||||||
|
14
|
||||||
|
True
|
||||||
|
-94.6987583319346
|
||||||
|
-98.0068958282471
|
||||||
|
0.371145516633987
|
||||||
|
0
|
||||||
|
-65.5345490773519
|
||||||
|
-48.0928927580516
|
||||||
|
0.0821696281433106
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-66.2971390088399
|
||||||
|
-48.0919455369314
|
||||||
|
0.0824445406595866
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-68.2659324010213
|
||||||
|
-47.9679103851318
|
||||||
|
0.0901949644088745
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-68.9330024560293
|
||||||
|
-47.9617745717367
|
||||||
|
0.0741503397623698
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-68.9996176083883
|
||||||
|
-47.9868227322896
|
||||||
|
0.0811579465866089
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-69.0741902987162
|
||||||
|
-47.9962577819824
|
||||||
|
0.0902631839116414
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-69.1185588876406
|
||||||
|
-48.0084948221842
|
||||||
|
0.0896445353825887
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-69.1932870864868
|
||||||
|
-48.0247398058573
|
||||||
|
0.0864912668863932
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-69.2399808883667
|
||||||
|
-48.0406180699666
|
||||||
|
0.0826785326004028
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-69.3729923248291
|
||||||
|
-48.0507068951925
|
||||||
|
0.0753742853800456
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-69.502007261912
|
||||||
|
-48.0152909914653
|
||||||
|
0.0562341809272766
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-69.6805796305339
|
||||||
|
-47.9529766718547
|
||||||
|
0.00605392009019852
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-69.7465653419495
|
||||||
|
-47.9553860028585
|
||||||
|
0.000755888223648071
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-69.917982506752
|
||||||
|
-47.9998155911764
|
||||||
|
0.00417213241259257
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-70.4033851623535
|
||||||
|
-48.1885171890259
|
||||||
|
0.0103249351183573
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-71.328177467982
|
||||||
|
-48.5185417175293
|
||||||
|
2.08333212261399E-05
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-72.7273258845011
|
||||||
|
-47.8933919270833
|
||||||
|
0.0094555139541626
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-72.7809668223063
|
||||||
|
-47.776501083374
|
||||||
|
0.0289373199144999
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-72.7527559280395
|
||||||
|
-47.166915734609
|
||||||
|
0.135223134358724
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-72.7196399370829
|
||||||
|
-47.071635723114
|
||||||
|
0.0760513146718343
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-72.7042126337687
|
||||||
|
-47.0472258249919
|
||||||
|
0.0588250835736593
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-72.6919200261434
|
||||||
|
-47.0329411188761
|
||||||
|
0.0555859247843424
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-72.5977428436279
|
||||||
|
-46.9303064982096
|
||||||
|
0.0106916089852651
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-71.9506707509359
|
||||||
|
-46.338755830129
|
||||||
|
2.08333212261399E-05
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-71.8265404701233
|
||||||
|
-46.2414363861084
|
||||||
|
2.08333212261399E-05
|
||||||
|
0
|
||||||
|
6
|
||||||
|
-71.8128362019857
|
||||||
|
-46.2284189224243
|
||||||
|
2.08333212261399E-05
|
||||||
|
0
|
||||||
|
Empyrean Facility Upper Level
|
||||||
|
14
|
||||||
|
True
|
||||||
|
-71.8083083470662
|
||||||
|
-46.2096791585286
|
||||||
|
-0.000262499845121056
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
uTank2 NAV 1.2
|
||||||
|
4
|
||||||
|
22
|
||||||
|
4
|
||||||
|
0.675179147720337
|
||||||
|
13.5718362808228
|
||||||
|
0.521687507629395
|
||||||
|
0
|
||||||
|
/ah
|
||||||
|
3
|
||||||
|
0.675179147720337
|
||||||
|
13.5718362808228
|
||||||
|
0.521687507629395
|
||||||
|
0
|
||||||
|
15000
|
||||||
|
0
|
||||||
|
59.3342878341675
|
||||||
|
-28.5975779215495
|
||||||
|
0.0500208298365275
|
||||||
|
0
|
||||||
|
0
|
||||||
|
59.35896803538
|
||||||
|
-28.6749758720398
|
||||||
|
0.0500208298365275
|
||||||
|
0
|
||||||
|
0
|
||||||
|
59.37918065389
|
||||||
|
-28.7118357340495
|
||||||
|
0.0500208298365275
|
||||||
|
0
|
||||||
|
6
|
||||||
|
0.675179147720337
|
||||||
|
13.5718362808228
|
||||||
|
0.521687507629395
|
||||||
|
0
|
||||||
|
Portal to Town Network
|
||||||
|
14
|
||||||
|
True
|
||||||
|
59.3936458587647
|
||||||
|
-28.7256083488464
|
||||||
|
0.0508250035345554
|
||||||
|
0
|
||||||
|
-101.617960484823
|
||||||
|
-96.6229596773783
|
||||||
|
2.08333134651184E-05
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.64374332428
|
||||||
|
-96.6113506952922
|
||||||
|
2.08333134651184E-05
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.677829106649
|
||||||
|
-96.6077419598897
|
||||||
|
2.08333134651184E-05
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.708999125163
|
||||||
|
-96.6393287976583
|
||||||
|
2.08333134651184E-05
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.720591862996
|
||||||
|
-96.644443766276
|
||||||
|
2.08333134651184E-05
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.724977588654
|
||||||
|
-96.6444894790649
|
||||||
|
2.08333134651184E-05
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.766201734543
|
||||||
|
-96.642304166158
|
||||||
|
2.08333134651184E-05
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.826050774256
|
||||||
|
-96.6380467732747
|
||||||
|
2.08333134651184E-05
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.873989931742
|
||||||
|
-96.6189005851746
|
||||||
|
2.08333134651184E-05
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.907649771372
|
||||||
|
-96.5925824801127
|
||||||
|
2.08333134651184E-05
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.907772219181
|
||||||
|
-96.5846062819163
|
||||||
|
2.08333134651184E-05
|
||||||
|
0
|
||||||
|
6
|
||||||
|
0.675179147720337
|
||||||
|
13.5718362808228
|
||||||
|
0.521687507629395
|
||||||
|
0
|
||||||
|
Portal to Zaikhal
|
||||||
|
14
|
||||||
|
True
|
||||||
|
-101.90858749946
|
||||||
|
-96.5742208321889
|
||||||
|
-0.000262499845121056
|
||||||
|
0
|
||||||
|
0.684857130050659
|
||||||
|
13.498526652654
|
||||||
|
0.516687520345052
|
||||||
|
0
|
||||||
|
0
|
||||||
|
0.648467270533244
|
||||||
|
13.6382585525513
|
||||||
|
0.516687520345052
|
||||||
|
0
|
||||||
|
0
|
||||||
|
0.663638496398926
|
||||||
|
13.6434263865153
|
||||||
|
0.516687520345052
|
||||||
|
0
|
||||||
|
7
|
||||||
|
0.675179147720337
|
||||||
|
13.5718362808228
|
||||||
|
0.521687507629395
|
||||||
|
0
|
||||||
|
Jondor Torgren
|
||||||
|
37
|
||||||
|
True
|
||||||
|
0.675
|
||||||
|
13.6436941782633
|
||||||
|
0.516687512397766
|
||||||
|
|
@ -0,0 +1,307 @@
|
||||||
|
uTank2 NAV 1.2
|
||||||
|
4
|
||||||
|
59
|
||||||
|
4
|
||||||
|
59.3306452433268
|
||||||
|
-28.5473848342896
|
||||||
|
0.0500208298365275
|
||||||
|
0
|
||||||
|
/ub equip load idle.utl
|
||||||
|
3
|
||||||
|
59.3306452433268
|
||||||
|
-28.5473848342896
|
||||||
|
0.0500208298365275
|
||||||
|
0
|
||||||
|
10000
|
||||||
|
4
|
||||||
|
59.3306452433268
|
||||||
|
-28.5473848342896
|
||||||
|
0.0500208298365275
|
||||||
|
0
|
||||||
|
/ub equip load quest.utl
|
||||||
|
3
|
||||||
|
59.3306452433268
|
||||||
|
-28.5473848342896
|
||||||
|
0.0500208298365275
|
||||||
|
0
|
||||||
|
10000
|
||||||
|
2
|
||||||
|
59.3306452433268
|
||||||
|
-28.5473848342896
|
||||||
|
0.0500208298365275
|
||||||
|
0
|
||||||
|
48
|
||||||
|
3
|
||||||
|
59.3306452433268
|
||||||
|
-28.5473848342896
|
||||||
|
0.0500208298365275
|
||||||
|
0
|
||||||
|
2000
|
||||||
|
0
|
||||||
|
-101.643393707275
|
||||||
|
17.8825289090474
|
||||||
|
2.08333134651184E-05
|
||||||
|
0
|
||||||
|
4
|
||||||
|
59.3306452433268
|
||||||
|
-28.5473848342896
|
||||||
|
0.0500208298365275
|
||||||
|
0
|
||||||
|
/ub jumpsw 300
|
||||||
|
0
|
||||||
|
-101.592045052846
|
||||||
|
17.8805678685506
|
||||||
|
-0.0999791701634725
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.554651578267
|
||||||
|
17.8813594023387
|
||||||
|
-0.0999791701634725
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.533706283569
|
||||||
|
17.8833907604218
|
||||||
|
-0.0999791701634725
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.534181054433
|
||||||
|
17.8413492997487
|
||||||
|
-0.0999791701634725
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.48626130422
|
||||||
|
17.841304953893
|
||||||
|
-0.0999791701634725
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.490094470978
|
||||||
|
17.8813687324524
|
||||||
|
-0.0999791701634725
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.407286643982
|
||||||
|
17.884686867396
|
||||||
|
-0.124979170163473
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.441320260366
|
||||||
|
17.801603158315
|
||||||
|
-0.124979170163473
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.466675154368
|
||||||
|
17.7969429810842
|
||||||
|
-0.124979170163473
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.516472848256
|
||||||
|
17.8459821859996
|
||||||
|
-0.124979170163473
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.531043752035
|
||||||
|
17.8853654225667
|
||||||
|
-0.124979170163473
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.575432650248
|
||||||
|
17.8846518675486
|
||||||
|
-0.124979170163473
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.575820668538
|
||||||
|
17.7591938654582
|
||||||
|
-0.149979162216187
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.529988797506
|
||||||
|
17.7609985669454
|
||||||
|
-0.149979162216187
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.53424466451
|
||||||
|
17.8423960208893
|
||||||
|
-0.174979162216187
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.583576202393
|
||||||
|
17.8416724999746
|
||||||
|
-0.174979162216187
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.57072356542
|
||||||
|
17.8728516896566
|
||||||
|
-0.174979162216187
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.595848846436
|
||||||
|
17.8807149728139
|
||||||
|
-0.174979162216187
|
||||||
|
0
|
||||||
|
4
|
||||||
|
59.3306452433268
|
||||||
|
-28.5473848342896
|
||||||
|
0.0500208298365275
|
||||||
|
0
|
||||||
|
/ub jumpsw 300
|
||||||
|
0
|
||||||
|
-101.622127024333
|
||||||
|
17.8804373105367
|
||||||
|
-0.274979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.616229724884
|
||||||
|
17.9339917182922
|
||||||
|
-0.274979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.612587674459
|
||||||
|
17.9819168567657
|
||||||
|
-0.296296882629395
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.615892442067
|
||||||
|
18.0119011123975
|
||||||
|
-0.299979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.590025297801
|
||||||
|
18.0101968288422
|
||||||
|
-0.303131580352783
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.529243914286
|
||||||
|
18.0071381568909
|
||||||
|
-0.324979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.53581682841
|
||||||
|
17.9724334478378
|
||||||
|
-0.324979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.572706063588
|
||||||
|
17.9642690896988
|
||||||
|
-0.324979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.578296979268
|
||||||
|
17.9251905759176
|
||||||
|
-0.324979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.61803188324
|
||||||
|
17.9257889827093
|
||||||
|
-0.324979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.620800844828
|
||||||
|
17.9667853593826
|
||||||
|
-0.324979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.617472012838
|
||||||
|
18.0454295317332
|
||||||
|
-0.349979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.660903453827
|
||||||
|
18.0499082385407
|
||||||
|
-0.349979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.658096440633
|
||||||
|
17.9865849812826
|
||||||
|
-0.374979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.658587837219
|
||||||
|
17.9719900369644
|
||||||
|
-0.374979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.69858379364
|
||||||
|
17.9644167820613
|
||||||
|
-0.374979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.706780131658
|
||||||
|
17.9288707415263
|
||||||
|
-0.374979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.743702141444
|
||||||
|
17.921234814326
|
||||||
|
-0.374979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.775855763753
|
||||||
|
17.9226232131322
|
||||||
|
-0.374979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.784734662374
|
||||||
|
17.9344542185465
|
||||||
|
-0.374979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.78315264384
|
||||||
|
18.0072518110275
|
||||||
|
-0.349979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.86580046018
|
||||||
|
18.008648399512
|
||||||
|
-0.374979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.888455645243
|
||||||
|
18.0071597099304
|
||||||
|
-0.374979146321615
|
||||||
|
0
|
||||||
|
4
|
||||||
|
59.3306452433268
|
||||||
|
-28.5473848342896
|
||||||
|
0.0500208298365275
|
||||||
|
0
|
||||||
|
/ub jumpsw 300
|
||||||
|
0
|
||||||
|
-101.913387115796
|
||||||
|
18.0075169881185
|
||||||
|
-0.474979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.889330867926
|
||||||
|
18.0084470907847
|
||||||
|
-0.474979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.844996301333
|
||||||
|
17.9643644889196
|
||||||
|
-0.474979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.825047651927
|
||||||
|
17.9635365645091
|
||||||
|
-0.474979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.821242467562
|
||||||
|
17.9419899145762
|
||||||
|
-0.474979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.786994679769
|
||||||
|
17.9041363716125
|
||||||
|
-0.474979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.782102791468
|
||||||
|
17.8818953514099
|
||||||
|
-0.474979146321615
|
||||||
|
0
|
||||||
|
0
|
||||||
|
-101.747181431452
|
||||||
|
17.8816454569499
|
||||||
|
-0.474979146321615
|
||||||
|
0
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
1
|
||||||
|
Spells
|
||||||
|
4
|
||||||
|
SpellID
|
||||||
|
EndTime
|
||||||
|
Target
|
||||||
|
CastTime
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
0
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
1
|
||||||
|
CondAct
|
||||||
|
5
|
||||||
|
CType
|
||||||
|
AType
|
||||||
|
CData
|
||||||
|
AData
|
||||||
|
State
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
0
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
uTank2 NAV 1.2
|
||||||
|
1
|
||||||
|
0
|
||||||
468
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/owner-a.usd
Normal file
468
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/owner-a.usd
Normal file
|
|
@ -0,0 +1,468 @@
|
||||||
|
8
|
||||||
|
AntiExtraBuffSpells
|
||||||
|
1
|
||||||
|
ExemplarId
|
||||||
|
n
|
||||||
|
0
|
||||||
|
AssistItems
|
||||||
|
2
|
||||||
|
Object
|
||||||
|
Type
|
||||||
|
n
|
||||||
|
n
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Bread
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Handy Healing Kit
|
||||||
|
i
|
||||||
|
0
|
||||||
|
BuffedItems
|
||||||
|
2
|
||||||
|
Object
|
||||||
|
Spell
|
||||||
|
n
|
||||||
|
n
|
||||||
|
11
|
||||||
|
i
|
||||||
|
-2147482735
|
||||||
|
i
|
||||||
|
1599
|
||||||
|
i
|
||||||
|
-2147482735
|
||||||
|
i
|
||||||
|
35
|
||||||
|
i
|
||||||
|
-2147482735
|
||||||
|
i
|
||||||
|
49
|
||||||
|
i
|
||||||
|
-2147482735
|
||||||
|
i
|
||||||
|
1587
|
||||||
|
i
|
||||||
|
-2147482739
|
||||||
|
i
|
||||||
|
1599
|
||||||
|
i
|
||||||
|
-2147482739
|
||||||
|
i
|
||||||
|
1475
|
||||||
|
i
|
||||||
|
-2147482739
|
||||||
|
i
|
||||||
|
3253
|
||||||
|
i
|
||||||
|
-2147482734
|
||||||
|
i
|
||||||
|
1599
|
||||||
|
i
|
||||||
|
-2147482734
|
||||||
|
i
|
||||||
|
35
|
||||||
|
i
|
||||||
|
-2147482734
|
||||||
|
i
|
||||||
|
49
|
||||||
|
i
|
||||||
|
-2147482734
|
||||||
|
i
|
||||||
|
1587
|
||||||
|
ExtraBuffSpells
|
||||||
|
1
|
||||||
|
ExemplarId
|
||||||
|
n
|
||||||
|
0
|
||||||
|
GemFoodItems
|
||||||
|
2
|
||||||
|
Name
|
||||||
|
Spell
|
||||||
|
n
|
||||||
|
n
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Asheron's Benediction
|
||||||
|
i
|
||||||
|
3810
|
||||||
|
s
|
||||||
|
Blackmoor's Favor
|
||||||
|
i
|
||||||
|
3811
|
||||||
|
ItemUseSpecifiers
|
||||||
|
2
|
||||||
|
Object
|
||||||
|
Uses
|
||||||
|
y
|
||||||
|
n
|
||||||
|
0
|
||||||
|
MyMonsters
|
||||||
|
21
|
||||||
|
MonsterName
|
||||||
|
AttackPriority
|
||||||
|
DamageType
|
||||||
|
WeaponToUse
|
||||||
|
Imperil
|
||||||
|
Vuln
|
||||||
|
Yield
|
||||||
|
GravityW
|
||||||
|
Attack
|
||||||
|
Ring
|
||||||
|
Broadside
|
||||||
|
Fester
|
||||||
|
WeakeningCurse
|
||||||
|
FesteringCurse
|
||||||
|
Corruption
|
||||||
|
DestructiveCurse
|
||||||
|
Corrosion
|
||||||
|
Streak
|
||||||
|
SecondaryVuln
|
||||||
|
SecondaryEquip
|
||||||
|
PetDamageType
|
||||||
|
y
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
<DEFAULT>
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
8
|
||||||
|
i
|
||||||
|
-1
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
True
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
True
|
||||||
|
i
|
||||||
|
98
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
101
|
||||||
|
Settings
|
||||||
|
2
|
||||||
|
Setting
|
||||||
|
Value
|
||||||
|
y
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
RechargeHandlerSet
|
||||||
|
TABLE
|
||||||
|
5
|
||||||
|
Vital
|
||||||
|
HandlerString
|
||||||
|
MinPercent
|
||||||
|
MaxPercent
|
||||||
|
Stance
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
26
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Stamina to Health
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
15
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Mana to Health
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
15
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
15
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
15
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Stamina to Health
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Mana to Health
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
15
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Stamina to Health
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
10
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
1
|
||||||
|
Spells
|
||||||
|
4
|
||||||
|
SpellID
|
||||||
|
EndTime
|
||||||
|
Target
|
||||||
|
CastTime
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
0
|
||||||
424
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/owner-b.usd
Normal file
424
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/owner-b.usd
Normal file
|
|
@ -0,0 +1,424 @@
|
||||||
|
8
|
||||||
|
AntiExtraBuffSpells
|
||||||
|
1
|
||||||
|
ExemplarId
|
||||||
|
n
|
||||||
|
0
|
||||||
|
AssistItems
|
||||||
|
2
|
||||||
|
Object
|
||||||
|
Type
|
||||||
|
n
|
||||||
|
n
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Adept Healing Kit
|
||||||
|
i
|
||||||
|
0
|
||||||
|
s
|
||||||
|
Bread
|
||||||
|
i
|
||||||
|
3
|
||||||
|
BuffedItems
|
||||||
|
2
|
||||||
|
Object
|
||||||
|
Spell
|
||||||
|
n
|
||||||
|
n
|
||||||
|
0
|
||||||
|
ExtraBuffSpells
|
||||||
|
1
|
||||||
|
ExemplarId
|
||||||
|
n
|
||||||
|
0
|
||||||
|
GemFoodItems
|
||||||
|
2
|
||||||
|
Name
|
||||||
|
Spell
|
||||||
|
n
|
||||||
|
n
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Asheron's Benediction
|
||||||
|
i
|
||||||
|
3810
|
||||||
|
s
|
||||||
|
Blackmoor's Favor
|
||||||
|
i
|
||||||
|
3811
|
||||||
|
ItemUseSpecifiers
|
||||||
|
2
|
||||||
|
Object
|
||||||
|
Uses
|
||||||
|
y
|
||||||
|
n
|
||||||
|
0
|
||||||
|
MyMonsters
|
||||||
|
21
|
||||||
|
MonsterName
|
||||||
|
AttackPriority
|
||||||
|
DamageType
|
||||||
|
WeaponToUse
|
||||||
|
Imperil
|
||||||
|
Vuln
|
||||||
|
Yield
|
||||||
|
GravityW
|
||||||
|
Attack
|
||||||
|
Ring
|
||||||
|
Broadside
|
||||||
|
Fester
|
||||||
|
WeakeningCurse
|
||||||
|
FesteringCurse
|
||||||
|
Corruption
|
||||||
|
DestructiveCurse
|
||||||
|
Corrosion
|
||||||
|
Streak
|
||||||
|
SecondaryVuln
|
||||||
|
SecondaryEquip
|
||||||
|
PetDamageType
|
||||||
|
y
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
<DEFAULT>
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
8
|
||||||
|
i
|
||||||
|
-1
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
True
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
True
|
||||||
|
i
|
||||||
|
98
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
101
|
||||||
|
Settings
|
||||||
|
2
|
||||||
|
Setting
|
||||||
|
Value
|
||||||
|
y
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
RechargeHandlerSet
|
||||||
|
TABLE
|
||||||
|
5
|
||||||
|
Vital
|
||||||
|
HandlerString
|
||||||
|
MinPercent
|
||||||
|
MaxPercent
|
||||||
|
Stance
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
26
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Stamina to Health
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
15
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Mana to Health
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
15
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
15
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
15
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Stamina to Health
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Mana to Health
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
15
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Stamina to Health
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
10
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
1
|
||||||
|
Spells
|
||||||
|
4
|
||||||
|
SpellID
|
||||||
|
EndTime
|
||||||
|
Target
|
||||||
|
CastTime
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
0
|
||||||
468
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/owner-c.usd
Normal file
468
tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/owner-c.usd
Normal file
|
|
@ -0,0 +1,468 @@
|
||||||
|
8
|
||||||
|
AntiExtraBuffSpells
|
||||||
|
1
|
||||||
|
ExemplarId
|
||||||
|
n
|
||||||
|
0
|
||||||
|
AssistItems
|
||||||
|
2
|
||||||
|
Object
|
||||||
|
Type
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Mana Draught
|
||||||
|
i
|
||||||
|
5
|
||||||
|
BuffedItems
|
||||||
|
2
|
||||||
|
Object
|
||||||
|
Spell
|
||||||
|
n
|
||||||
|
n
|
||||||
|
10
|
||||||
|
i
|
||||||
|
-2147465511
|
||||||
|
i
|
||||||
|
1599
|
||||||
|
i
|
||||||
|
-2147465511
|
||||||
|
i
|
||||||
|
35
|
||||||
|
i
|
||||||
|
-2147465511
|
||||||
|
i
|
||||||
|
49
|
||||||
|
i
|
||||||
|
-2147465511
|
||||||
|
i
|
||||||
|
1587
|
||||||
|
i
|
||||||
|
-2147465453
|
||||||
|
i
|
||||||
|
1599
|
||||||
|
i
|
||||||
|
-2147465453
|
||||||
|
i
|
||||||
|
1475
|
||||||
|
i
|
||||||
|
-2147465453
|
||||||
|
i
|
||||||
|
3253
|
||||||
|
i
|
||||||
|
-2147482575
|
||||||
|
i
|
||||||
|
1599
|
||||||
|
i
|
||||||
|
-2147482575
|
||||||
|
i
|
||||||
|
35
|
||||||
|
i
|
||||||
|
-2147482575
|
||||||
|
i
|
||||||
|
49
|
||||||
|
ExtraBuffSpells
|
||||||
|
1
|
||||||
|
ExemplarId
|
||||||
|
n
|
||||||
|
0
|
||||||
|
GemFoodItems
|
||||||
|
2
|
||||||
|
Name
|
||||||
|
Spell
|
||||||
|
n
|
||||||
|
n
|
||||||
|
4
|
||||||
|
s
|
||||||
|
Asheron's Benediction
|
||||||
|
i
|
||||||
|
3810
|
||||||
|
s
|
||||||
|
Blackmoor's Favor
|
||||||
|
i
|
||||||
|
3811
|
||||||
|
s
|
||||||
|
White Sapphire
|
||||||
|
i
|
||||||
|
2053
|
||||||
|
s
|
||||||
|
Tourmaline
|
||||||
|
i
|
||||||
|
169
|
||||||
|
ItemUseSpecifiers
|
||||||
|
2
|
||||||
|
Object
|
||||||
|
Uses
|
||||||
|
y
|
||||||
|
n
|
||||||
|
0
|
||||||
|
MyMonsters
|
||||||
|
21
|
||||||
|
MonsterName
|
||||||
|
AttackPriority
|
||||||
|
DamageType
|
||||||
|
WeaponToUse
|
||||||
|
Imperil
|
||||||
|
Vuln
|
||||||
|
Yield
|
||||||
|
GravityW
|
||||||
|
Attack
|
||||||
|
Ring
|
||||||
|
Broadside
|
||||||
|
Fester
|
||||||
|
WeakeningCurse
|
||||||
|
FesteringCurse
|
||||||
|
Corruption
|
||||||
|
DestructiveCurse
|
||||||
|
Corrosion
|
||||||
|
Streak
|
||||||
|
SecondaryVuln
|
||||||
|
SecondaryEquip
|
||||||
|
PetDamageType
|
||||||
|
y
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
<DEFAULT>
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
8
|
||||||
|
i
|
||||||
|
-1
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
True
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
False
|
||||||
|
b
|
||||||
|
True
|
||||||
|
i
|
||||||
|
98
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
101
|
||||||
|
Settings
|
||||||
|
2
|
||||||
|
Setting
|
||||||
|
Value
|
||||||
|
y
|
||||||
|
n
|
||||||
|
1
|
||||||
|
s
|
||||||
|
RechargeHandlerSet
|
||||||
|
TABLE
|
||||||
|
5
|
||||||
|
Vital
|
||||||
|
HandlerString
|
||||||
|
MinPercent
|
||||||
|
MaxPercent
|
||||||
|
Stance
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
n
|
||||||
|
26
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Stamina to Health
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
15
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Mana to Health
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
15
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
15
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
15
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Stamina to Health
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Mana to Health
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
15
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Stamina to Health
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
10
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
1
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
2
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
1
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Kit Recharge
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Recharge With Food
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
|
i
|
||||||
|
3
|
||||||
|
s
|
||||||
|
Regular Spell
|
||||||
|
i
|
||||||
|
0
|
||||||
|
i
|
||||||
|
100
|
||||||
|
i
|
||||||
|
2
|
||||||
691
tests/AcDream.Plugins.MossTank.Tests/MetafSerializerTests.cs
Normal file
691
tests/AcDream.Plugins.MossTank.Tests/MetafSerializerTests.cs
Normal file
|
|
@ -0,0 +1,691 @@
|
||||||
|
using AcDream.Plugin.Abstractions;
|
||||||
|
|
||||||
|
namespace AcDream.Plugins.MossTank.Tests;
|
||||||
|
|
||||||
|
public sealed class MetafSerializerTests
|
||||||
|
{
|
||||||
|
private static readonly string FixturesRoot = Path.Combine(
|
||||||
|
AppContext.BaseDirectory, "Fixtures", "vtank");
|
||||||
|
|
||||||
|
private static string[] AllAfFixtures() => Directory.GetFiles(
|
||||||
|
Path.Combine(FixturesRoot, "af"), "*.af");
|
||||||
|
|
||||||
|
private static string[] NavOnlyAfFixtures() => Directory.GetFiles(
|
||||||
|
Path.Combine(FixturesRoot, "af"), "nav_*.af");
|
||||||
|
|
||||||
|
private static string[] MetaAfFixtures() => Directory.GetFiles(
|
||||||
|
Path.Combine(FixturesRoot, "af"), "*.af")
|
||||||
|
.Where(static path => !Path.GetFileName(path).StartsWith(
|
||||||
|
"nav_", StringComparison.Ordinal))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
public static TheoryData<string> AllAfFixtureData()
|
||||||
|
{
|
||||||
|
var data = new TheoryData<string>();
|
||||||
|
foreach (string path in AllAfFixtures())
|
||||||
|
data.Add(path);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData<string> MetaAfFixtureData()
|
||||||
|
{
|
||||||
|
var data = new TheoryData<string>();
|
||||||
|
foreach (string path in MetaAfFixtures())
|
||||||
|
data.Add(path);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData<string> NavOnlyAfFixtureData()
|
||||||
|
{
|
||||||
|
var data = new TheoryData<string>();
|
||||||
|
foreach (string path in NavOnlyAfFixtures())
|
||||||
|
data.Add(path);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proof (1): every .af under the fixture set parses without error.
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(MetaAfFixtureData))]
|
||||||
|
public void EveryMetaAfFixtureParses(string path)
|
||||||
|
{
|
||||||
|
string text = File.ReadAllText(path);
|
||||||
|
Assert.True(
|
||||||
|
MetafSerializer.TryLoadMeta(text, NoOpSpellCatalog.Instance, out MetaProfile profile, out string error),
|
||||||
|
$"{Path.GetFileName(path)}: {error}");
|
||||||
|
Assert.NotEmpty(profile.Rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(NavOnlyAfFixtureData))]
|
||||||
|
public void EveryNavOnlyAfFixtureParses(string path)
|
||||||
|
{
|
||||||
|
string text = File.ReadAllText(path);
|
||||||
|
var target = new NavigationSettings();
|
||||||
|
Assert.True(
|
||||||
|
MetafSerializer.TryLoadNav(text, target, NoOpSpellCatalog.Instance, out string error),
|
||||||
|
$"{Path.GetFileName(path)}: {error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proof (2): parse -> write -> parse is identical (model equality; the
|
||||||
|
// writer's own byte-for-byte shape is proof (4), below).
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(MetaAfFixtureData))]
|
||||||
|
public void MetaParseWriteParseIsIdentical(string path)
|
||||||
|
{
|
||||||
|
string text = File.ReadAllText(path);
|
||||||
|
Assert.True(MetafSerializer.TryLoadMeta(
|
||||||
|
text, NoOpSpellCatalog.Instance, out MetaProfile first, out string error1), error1);
|
||||||
|
string rewritten = MetafSerializer.SaveMeta(first);
|
||||||
|
Assert.True(MetafSerializer.TryLoadMeta(
|
||||||
|
rewritten, NoOpSpellCatalog.Instance, out MetaProfile second, out string error2), error2);
|
||||||
|
AssertProfilesEqual(first, second);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(NavOnlyAfFixtureData))]
|
||||||
|
public void NavParseWriteParseIsIdentical(string path)
|
||||||
|
{
|
||||||
|
string text = File.ReadAllText(path);
|
||||||
|
var first = new NavigationSettings();
|
||||||
|
Assert.True(MetafSerializer.TryLoadNav(
|
||||||
|
text, first, NoOpSpellCatalog.Instance, out string error1), error1);
|
||||||
|
string rewritten = MetafSerializer.SaveNav(first);
|
||||||
|
var second = new NavigationSettings();
|
||||||
|
Assert.True(MetafSerializer.TryLoadNav(
|
||||||
|
rewritten, second, NoOpSpellCatalog.Instance, out string error2), error2);
|
||||||
|
AssertNavigationEqual(first, second);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proof (3): for the met/ and nav/ samples that have a matching af/
|
||||||
|
// conversion, our model loaded from the binary import path equals our
|
||||||
|
// model loaded from metaf's own .af conversion.
|
||||||
|
public static TheoryData<string, string> MetMatchingAfPairs()
|
||||||
|
{
|
||||||
|
var data = new TheoryData<string, string>();
|
||||||
|
string metDir = Path.Combine(FixturesRoot, "met");
|
||||||
|
string afDir = Path.Combine(FixturesRoot, "af");
|
||||||
|
foreach (string metPath in Directory.GetFiles(metDir, "*.met"))
|
||||||
|
{
|
||||||
|
string name = Path.GetFileNameWithoutExtension(metPath);
|
||||||
|
string afPath = Path.Combine(afDir, name + ".af");
|
||||||
|
if (File.Exists(afPath))
|
||||||
|
data.Add(metPath, afPath);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(MetMatchingAfPairs))]
|
||||||
|
public void BinaryImportMatchesMetafAfConversion(string metPath, string afPath)
|
||||||
|
{
|
||||||
|
string metText = File.ReadAllText(metPath);
|
||||||
|
Assert.True(
|
||||||
|
VtankMetaProfileSerializer.TryLoad(metText, out MetaProfile fromMet, out string metError),
|
||||||
|
$"{Path.GetFileName(metPath)}: {metError}");
|
||||||
|
|
||||||
|
string afText = File.ReadAllText(afPath);
|
||||||
|
Assert.True(
|
||||||
|
MetafSerializer.TryLoadMeta(afText, NoOpSpellCatalog.Instance, out MetaProfile fromAf, out string afError),
|
||||||
|
$"{Path.GetFileName(afPath)}: {afError}");
|
||||||
|
|
||||||
|
AssertProfilesEqual(fromMet, fromAf);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData<string, string> NavMatchingAfPairs()
|
||||||
|
{
|
||||||
|
var data = new TheoryData<string, string>();
|
||||||
|
string navDir = Path.Combine(FixturesRoot, "nav");
|
||||||
|
string afDir = Path.Combine(FixturesRoot, "af");
|
||||||
|
foreach (string navPath in Directory.GetFiles(navDir, "*.nav"))
|
||||||
|
{
|
||||||
|
string name = Path.GetFileNameWithoutExtension(navPath);
|
||||||
|
string afPath = Path.Combine(afDir, name + ".af");
|
||||||
|
if (File.Exists(afPath))
|
||||||
|
data.Add(navPath, afPath);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(NavMatchingAfPairs))]
|
||||||
|
public void BinaryNavImportMatchesMetafAfConversion(string navPath, string afPath)
|
||||||
|
{
|
||||||
|
string navText = File.ReadAllText(navPath);
|
||||||
|
var fromNav = new NavigationSettings();
|
||||||
|
Assert.True(
|
||||||
|
VtankNavRouteSerializer.TryLoad(navText, fromNav, NoOpSpellCatalog.Instance, out string navError),
|
||||||
|
$"{Path.GetFileName(navPath)}: {navError}");
|
||||||
|
|
||||||
|
string afText = File.ReadAllText(afPath);
|
||||||
|
var fromAf = new NavigationSettings();
|
||||||
|
Assert.True(
|
||||||
|
MetafSerializer.TryLoadNav(afText, fromAf, NoOpSpellCatalog.Instance, out string afError),
|
||||||
|
$"{Path.GetFileName(afPath)}: {afError}");
|
||||||
|
|
||||||
|
AssertNavigationEqual(fromNav, fromAf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proof (4): the writer's output is REAL byte-identical to metaf's own
|
||||||
|
// canonical emission (item E, slice-1 fix round: this used to strip
|
||||||
|
// "~~" comments before comparing, which hid two facts — metaf's own
|
||||||
|
// ExportToMetAF DOES mechanically emit the auto-completion banner
|
||||||
|
// (OutputText.metaHeader, metaf_monolithic.py:365-406) plus a "~~ {"/
|
||||||
|
// "~~ }" editor-fold pair around every STATE:/NAV: block
|
||||||
|
// (State.ExportToMetAF py:12463-12467, Nav.ExportToMetAF py:12050-12054),
|
||||||
|
// which this writer now reproduces; and several committed fixtures do
|
||||||
|
// NOT carry metaf's fresh canonical header at all, for reasons that
|
||||||
|
// have nothing to do with this writer's correctness — see below).
|
||||||
|
public static TheoryData<string> ByteIdenticalFixtureData()
|
||||||
|
{
|
||||||
|
var data = new TheoryData<string>();
|
||||||
|
// Excluded, all for reasons independent of this writer (confirmed
|
||||||
|
// by inspecting each file's own first bytes, not asserted):
|
||||||
|
//
|
||||||
|
// bore_enhanced.af / bore_quest.af: hand-edited after generation —
|
||||||
|
// some IF:/DO: lines use a space instead of metaf's own tab
|
||||||
|
// separator (bore_quest.af line 9: "\tIF: Death", not
|
||||||
|
// "\tIF:\tDeath"; metaf's Rule.ExportToMetAF always joins with a
|
||||||
|
// tab, py:12371/12373).
|
||||||
|
//
|
||||||
|
// aphus.af, neftet.af, hunting.af, follower.af: the header's
|
||||||
|
// internal line breaks are metaf's own bare "\n" for a freshly
|
||||||
|
// generated file (confirmed against augments.af/bella.af/
|
||||||
|
// gauntlet_leader.af/empyrean_facility.af/example_sort_meta.af,
|
||||||
|
// whose first 8 bytes are "~~ {\n~~ "), but these four instead open
|
||||||
|
// with "~~ {\r\n~~ " (aphus/neftet/follower) or, for hunting.af, a
|
||||||
|
// wholly custom banner ("~~ Hunting Meta - Solo lifestone
|
||||||
|
// grinding..." instead of "~~ FOR AUTO-COMPLETION ASSISTANCE...").
|
||||||
|
// Both are evidence the file was re-saved by something other than
|
||||||
|
// a fresh metaf conversion (a text editor normalizing every line
|
||||||
|
// ending to CRLF loses metaf's internal bare-LF header formatting;
|
||||||
|
// a custom banner is an intentional hand edit) — not something
|
||||||
|
// this writer could or should reproduce.
|
||||||
|
//
|
||||||
|
// lockandkey.af: opens with a fully custom multi-line description
|
||||||
|
// ("~~\n~~\tlockandkey.af - Portal Bot with Stipend..."), never
|
||||||
|
// metaf's own banner at all.
|
||||||
|
//
|
||||||
|
// All five still exercise every other proof (parse,
|
||||||
|
// parse-write-parse, and — for aphus/augments/lockandkey/neftet —
|
||||||
|
// the ptl/tlk two-triple fidelity direct assertions in
|
||||||
|
// PtlNodeKeepsBothCoordinateTriplesDistinct) normally.
|
||||||
|
foreach (string name in new[]
|
||||||
|
{
|
||||||
|
"bella", "gauntlet_leader", "empyrean_facility",
|
||||||
|
"augments", "example_sort_meta",
|
||||||
|
})
|
||||||
|
{
|
||||||
|
data.Add(Path.Combine(FixturesRoot, "af", name + ".af"));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ByteIdenticalFixtureData))]
|
||||||
|
public void WriterOutputMatchesMetafCanonicalEmission(string path)
|
||||||
|
{
|
||||||
|
string original = File.ReadAllText(path);
|
||||||
|
Assert.True(
|
||||||
|
MetafSerializer.TryLoadMeta(original, NoOpSpellCatalog.Instance, out MetaProfile profile, out string error),
|
||||||
|
error);
|
||||||
|
string rewritten = MetafSerializer.SaveMeta(profile);
|
||||||
|
Assert.Equal(original, rewritten);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proof (4), nav-only case: SaveNav's own header (navHeader) and
|
||||||
|
// single-Nav fold-marker wrap are byte-identical to a real nav-only
|
||||||
|
// .af fixture. Every listed fixture carries metaf's own fresh banner
|
||||||
|
// (first bytes "~~ {\n~~ "); round 3 item 8 extended this from a
|
||||||
|
// single fixture (nav_ab.af) to the three checkpoint/recall/portal2
|
||||||
|
// fixtures copied from the metas repo (nav_briennecarlus.af carries
|
||||||
|
// chk+jmp, nav_empyrean.af carries rcl+ptl, nav_lockandkeyjaw.af
|
||||||
|
// carries rcl+chk) so the theory actually exercises every node kind
|
||||||
|
// metaf's own grammar supports, not just plain point waypoints.
|
||||||
|
public static TheoryData<string> NavByteIdenticalFixtureData()
|
||||||
|
{
|
||||||
|
var data = new TheoryData<string>();
|
||||||
|
foreach (string name in new[]
|
||||||
|
{
|
||||||
|
"nav_ab", "nav_briennecarlus", "nav_empyrean", "nav_lockandkeyjaw",
|
||||||
|
})
|
||||||
|
{
|
||||||
|
data.Add(Path.Combine(FixturesRoot, "af", name + ".af"));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(NavByteIdenticalFixtureData))]
|
||||||
|
public void WriterOutputMatchesMetafCanonicalEmissionNavOnly(string path)
|
||||||
|
{
|
||||||
|
string original = File.ReadAllText(path);
|
||||||
|
var settings = new NavigationSettings();
|
||||||
|
Assert.True(
|
||||||
|
MetafSerializer.TryLoadNav(original, settings, NoOpSpellCatalog.Instance, out string error),
|
||||||
|
error);
|
||||||
|
string rewritten = MetafSerializer.SaveNav(settings);
|
||||||
|
Assert.Equal(original, rewritten);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExampleSortMetaBinaryImportMatchesAf()
|
||||||
|
{
|
||||||
|
string metText = File.ReadAllText(Path.Combine(FixturesRoot, "met", "example_sort_meta.met"));
|
||||||
|
Assert.True(VtankMetaProfileSerializer.TryLoad(metText, out MetaProfile fromMet, out string metError), metError);
|
||||||
|
|
||||||
|
string afText = File.ReadAllText(Path.Combine(FixturesRoot, "af", "example_sort_meta.af"));
|
||||||
|
Assert.True(
|
||||||
|
MetafSerializer.TryLoadMeta(afText, NoOpSpellCatalog.Instance, out MetaProfile fromAf, out string afError),
|
||||||
|
afError);
|
||||||
|
|
||||||
|
AssertProfilesEqual(fromMet, fromAf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// aphus.af's embedded "nav0__stipend_nav" route carries a real "ptl"
|
||||||
|
// node whose two coordinate triples genuinely differ (line 1026:
|
||||||
|
// "-101.597905190786 -96.6216093699137 2.08333134651184E-05" myxyz,
|
||||||
|
// "59.3936458587647 -28.7256083488464 0.0508250035345554" tgtxyz). A
|
||||||
|
// prior port collapsed both onto RouteWaypoint.Position, which this
|
||||||
|
// test would have caught immediately: before the fix, Position held
|
||||||
|
// 59.39/-28.72/0.0508 (the second triple) and there was nowhere to
|
||||||
|
// read -101.59/-96.62/2.08e-05 back from at all.
|
||||||
|
[Fact]
|
||||||
|
public void PtlNodeKeepsBothCoordinateTriplesDistinct()
|
||||||
|
{
|
||||||
|
string original = File.ReadAllText(Path.Combine(FixturesRoot, "af", "aphus.af"));
|
||||||
|
Assert.True(
|
||||||
|
MetafSerializer.TryLoadMeta(original, NoOpSpellCatalog.Instance, out MetaProfile profile, out string error),
|
||||||
|
error);
|
||||||
|
|
||||||
|
RouteWaypoint waypoint = FindNavWaypoint(profile, "Portal to Town Network");
|
||||||
|
|
||||||
|
Assert.Equal(-101.597905190786d, waypoint.Position.EastWest, 6);
|
||||||
|
Assert.Equal(-96.6216093699137d, waypoint.Position.NorthSouth, 6);
|
||||||
|
Assert.Equal(2.08333134651184E-05d, waypoint.Position.Elevation, 10);
|
||||||
|
Assert.Equal(59.3936458587647d, waypoint.ReferencePosition.EastWest, 6);
|
||||||
|
Assert.Equal(-28.7256083488464d, waypoint.ReferencePosition.NorthSouth, 6);
|
||||||
|
Assert.Equal(0.0508250035345554d, waypoint.ReferencePosition.Elevation, 6);
|
||||||
|
|
||||||
|
string rewritten = MetafSerializer.SaveMeta(profile);
|
||||||
|
Assert.Contains(
|
||||||
|
"ptl -101.597905190786 -96.6216093699137 2.08333134651184E-05 "
|
||||||
|
+ "59.3936458587647 -28.7256083488464 0.0508250035345554 14 "
|
||||||
|
+ "{Portal to Town Network}",
|
||||||
|
rewritten,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Walks every <see cref="MetaActionKind.LoadEmbeddedNavigationRoute"/>
|
||||||
|
/// action's typed <see cref="MetaAction.EmbeddedRoute"/> looking for a
|
||||||
|
/// waypoint by object name.
|
||||||
|
/// </summary>
|
||||||
|
private static RouteWaypoint FindNavWaypoint(MetaProfile profile, string objectName)
|
||||||
|
{
|
||||||
|
foreach (MetaRule rule in profile.Rules)
|
||||||
|
{
|
||||||
|
RouteWaypoint? found = FindNavWaypoint(rule.Action, objectName);
|
||||||
|
if (found is not null)
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException($"no nav waypoint named '{objectName}' found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RouteWaypoint? FindNavWaypoint(MetaAction action, string objectName)
|
||||||
|
{
|
||||||
|
if (action.Kind == MetaActionKind.LoadEmbeddedNavigationRoute
|
||||||
|
&& action.EmbeddedRoute is { } nav)
|
||||||
|
{
|
||||||
|
foreach (RouteWaypoint waypoint in nav.Waypoints)
|
||||||
|
{
|
||||||
|
if (waypoint.ObjectName == objectName)
|
||||||
|
return waypoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (MetaAction child in action.Children)
|
||||||
|
{
|
||||||
|
RouteWaypoint? found = FindNavWaypoint(child, objectName);
|
||||||
|
if (found is not null)
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item H (slice-1 fix round): real VTank/metaf has no marker for a
|
||||||
|
// disabled rule at all, so SaveMeta refuses by default rather than
|
||||||
|
// silently dropping it — see docs/research/vtank-kb/
|
||||||
|
// 07-meta-and-expressions.md section 5, gap 6.
|
||||||
|
[Fact]
|
||||||
|
public void SaveMetaRefusesToDropADisabledRuleByDefault()
|
||||||
|
{
|
||||||
|
var profile = new MetaProfile
|
||||||
|
{
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new MetaRule
|
||||||
|
{
|
||||||
|
Enabled = false,
|
||||||
|
Condition = MetaCondition.Always(),
|
||||||
|
Action = new MetaAction
|
||||||
|
{
|
||||||
|
Kind = MetaActionKind.ChatCommand,
|
||||||
|
Text = "/say must not silently vanish",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
InvalidOperationException thrown = Assert.Throws<InvalidOperationException>(
|
||||||
|
() => MetafSerializer.SaveMeta(profile));
|
||||||
|
Assert.Contains("1 disabled rule", thrown.Message, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SaveMetaDropsDisabledRulesOnlyWhenExplicitlyToldTo()
|
||||||
|
{
|
||||||
|
var profile = new MetaProfile
|
||||||
|
{
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new MetaRule
|
||||||
|
{
|
||||||
|
Enabled = false,
|
||||||
|
Condition = MetaCondition.Always(),
|
||||||
|
Action = new MetaAction
|
||||||
|
{
|
||||||
|
Kind = MetaActionKind.ChatCommand,
|
||||||
|
Text = "/say must not run",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
string source = MetafSerializer.SaveMeta(profile, dropDisabledRules: true);
|
||||||
|
|
||||||
|
Assert.DoesNotContain("must not run", source, StringComparison.Ordinal);
|
||||||
|
Assert.True(MetafSerializer.TryLoadMeta(
|
||||||
|
source, NoOpSpellCatalog.Instance, out MetaProfile loaded, out string error), error);
|
||||||
|
Assert.Empty(loaded.Rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NestedAllAnyNotParsesCorrectly()
|
||||||
|
{
|
||||||
|
string af = string.Join("\r\n",
|
||||||
|
[
|
||||||
|
"STATE: {Default}",
|
||||||
|
"\tIF:\tAll",
|
||||||
|
"\t\t\tAlways",
|
||||||
|
"\t\t\tNot Death",
|
||||||
|
"\t\t\tAny",
|
||||||
|
"\t\t\t\tVendorOpen",
|
||||||
|
"\t\t\t\tVendorClosed",
|
||||||
|
"\t\tDO:\tDoAll",
|
||||||
|
"\t\t\t\tChat {hello}",
|
||||||
|
"\t\t\t\tSetState {Next}",
|
||||||
|
]) + "\r\n";
|
||||||
|
Assert.True(
|
||||||
|
MetafSerializer.TryLoadMeta(af, NoOpSpellCatalog.Instance, out MetaProfile profile, out string error),
|
||||||
|
error);
|
||||||
|
MetaRule rule = Assert.Single(profile.Rules);
|
||||||
|
Assert.Equal(MetaConditionKind.All, rule.Condition.Kind);
|
||||||
|
Assert.Equal(3, rule.Condition.Children.Count);
|
||||||
|
Assert.Equal(MetaConditionKind.Always, rule.Condition.Children[0].Kind);
|
||||||
|
Assert.Equal(MetaConditionKind.Not, rule.Condition.Children[1].Kind);
|
||||||
|
Assert.Equal(MetaConditionKind.CharacterDeath, rule.Condition.Children[1].Children[0].Kind);
|
||||||
|
Assert.Equal(MetaConditionKind.Any, rule.Condition.Children[2].Kind);
|
||||||
|
Assert.Equal(2, rule.Condition.Children[2].Children.Count);
|
||||||
|
Assert.Equal(MetaActionKind.All, rule.Action.Kind);
|
||||||
|
Assert.Equal(2, rule.Action.Children.Count);
|
||||||
|
Assert.Equal("hello", rule.Action.Children[0].Text);
|
||||||
|
Assert.Equal("Next", rule.Action.Children[1].Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item J (slice-1 fix round): MobsInDist_Priority is never exercised by
|
||||||
|
// any committed real fixture (only its name appears, in the
|
||||||
|
// auto-completion header banner text) — this synthesized round trip
|
||||||
|
// pins the argument order (count, distance, priority — see the
|
||||||
|
// regex-table comment in MetafSerializer.cs) with three DISTINCT
|
||||||
|
// numeric values so an accidental swap of any two fields fails loudly
|
||||||
|
// instead of silently agreeing with itself.
|
||||||
|
[Fact]
|
||||||
|
public void MobsInDistPriorityRoundTripsAllThreeNumbersDistinctly()
|
||||||
|
{
|
||||||
|
string af = string.Join("\r\n",
|
||||||
|
[
|
||||||
|
"STATE: {Default}",
|
||||||
|
"\tIF:\tMobsInDist_Priority 7 12.5 3",
|
||||||
|
"\t\tDO:\tChat {seen}",
|
||||||
|
]) + "\r\n";
|
||||||
|
|
||||||
|
Assert.True(
|
||||||
|
MetafSerializer.TryLoadMeta(af, NoOpSpellCatalog.Instance, out MetaProfile profile, out string error),
|
||||||
|
error);
|
||||||
|
MetaRule rule = Assert.Single(profile.Rules);
|
||||||
|
Assert.Equal(MetaConditionKind.MonsterPriorityCountWithinDistance, rule.Condition.Kind);
|
||||||
|
Assert.Equal(7, rule.Condition.Number);
|
||||||
|
Assert.Equal(12.5, rule.Condition.SecondaryNumber);
|
||||||
|
Assert.Equal(3, rule.Condition.TertiaryNumber);
|
||||||
|
|
||||||
|
string rewritten = MetafSerializer.SaveMeta(profile);
|
||||||
|
Assert.True(
|
||||||
|
MetafSerializer.TryLoadMeta(rewritten, NoOpSpellCatalog.Instance, out MetaProfile reloaded, out string error2),
|
||||||
|
error2);
|
||||||
|
MetaRule reloadedRule = Assert.Single(reloaded.Rules);
|
||||||
|
Assert.Equal(7, reloadedRule.Condition.Number);
|
||||||
|
Assert.Equal(12.5, reloadedRule.Condition.SecondaryNumber);
|
||||||
|
Assert.Equal(3, reloadedRule.Condition.TertiaryNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SynthesizedGetOptFollowAndJumpFixtureRoundTrips()
|
||||||
|
{
|
||||||
|
// GetOpt, the "flw" nav node, and "jmp" never appear in any real
|
||||||
|
// fixture in metas/af (grep confirmed at slice-1 authoring time), so
|
||||||
|
// this small hand-authored fixture exercises them — validated as
|
||||||
|
// real metaf grammar via `py metaf_monolithic.py` during authoring
|
||||||
|
// (not re-run automatically here, since python is not guaranteed on
|
||||||
|
// the Ubuntu portable CI lane).
|
||||||
|
string af = string.Join("\r\n",
|
||||||
|
[
|
||||||
|
"STATE: {Default}",
|
||||||
|
"\tIF:\tAlways",
|
||||||
|
"\t\tDO:\tDoAll",
|
||||||
|
"\t\t\t\tGetOpt {AttackDistance} {myvar}",
|
||||||
|
"\t\t\t\tSetOpt {AttackDistance} {myvar}",
|
||||||
|
"NAV: myfollow follow",
|
||||||
|
"\tflw 00001234 {Some Monster}",
|
||||||
|
]) + "\r\n";
|
||||||
|
Assert.True(
|
||||||
|
MetafSerializer.TryLoadMeta(af, NoOpSpellCatalog.Instance, out MetaProfile profile, out string error),
|
||||||
|
error);
|
||||||
|
MetaRule rule = Assert.Single(profile.Rules);
|
||||||
|
MetaAction getOpt = rule.Action.Children[0];
|
||||||
|
MetaAction setOpt = rule.Action.Children[1];
|
||||||
|
Assert.Equal(MetaActionKind.GetVtankOption, getOpt.Kind);
|
||||||
|
Assert.Equal("AttackDistance", getOpt.Text);
|
||||||
|
Assert.Equal("myvar", getOpt.SecondaryText);
|
||||||
|
Assert.Equal(MetaActionKind.SetVtankOption, setOpt.Kind);
|
||||||
|
|
||||||
|
var nav = new NavigationSettings();
|
||||||
|
Assert.True(
|
||||||
|
MetafSerializer.TryLoadNav(af, nav, NoOpSpellCatalog.Instance, out string navError),
|
||||||
|
navError);
|
||||||
|
Assert.Equal(RouteMode.Target, nav.Mode);
|
||||||
|
Assert.Equal(0x00001234u, nav.FollowTargetObjectId);
|
||||||
|
Assert.Equal("Some Monster", nav.FollowTargetName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round 3 item 5: the retail 2000 ms jump-charge ceiling
|
||||||
|
/// (refs/vtank/decompiled/bi.cs:502-505) is EXECUTION-time behavior
|
||||||
|
/// (NavigationController.TickJump), not a storage-format limit — metaf
|
||||||
|
/// and VTank both round-trip the authored value verbatim
|
||||||
|
/// (metaf_monolithic.py:11708-11820, NJump — no clamp at all). An
|
||||||
|
/// authored 5000 ms waypoint must therefore survive an .af load
|
||||||
|
/// unchanged; renamed from JumpNodeClampsChargeMillisecondsTo2000,
|
||||||
|
/// which pinned the pre-fix (wrong) load-time clamp.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void JumpNodeLoadPreservesAuthoredChargeMillisecondsAboveRetailCeiling()
|
||||||
|
{
|
||||||
|
const string af = """
|
||||||
|
NAV: j once
|
||||||
|
jmp 1 2 3 90 {True} 5000
|
||||||
|
""";
|
||||||
|
var target = new NavigationSettings();
|
||||||
|
Assert.True(MetafSerializer.TryLoadNav(af, target, NoOpSpellCatalog.Instance, out string error), error);
|
||||||
|
RouteWaypoint waypoint = Assert.Single(target.Waypoints);
|
||||||
|
Assert.Equal(RouteWaypointType.Jump, waypoint.Type);
|
||||||
|
Assert.Equal(5000, waypoint.JumpChargeMilliseconds);
|
||||||
|
Assert.True(waypoint.JumpRun);
|
||||||
|
Assert.Equal(90f, waypoint.JumpHeadingDegrees);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JumpNodeBelowClampPassesThroughUnchanged()
|
||||||
|
{
|
||||||
|
const string af = """
|
||||||
|
NAV: j once
|
||||||
|
jmp 1 2 3 90 {False} 500
|
||||||
|
""";
|
||||||
|
var target = new NavigationSettings();
|
||||||
|
Assert.True(MetafSerializer.TryLoadNav(af, target, NoOpSpellCatalog.Instance, out string error), error);
|
||||||
|
RouteWaypoint waypoint = Assert.Single(target.Waypoints);
|
||||||
|
Assert.Equal(500, waypoint.JumpChargeMilliseconds);
|
||||||
|
Assert.False(waypoint.JumpRun);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round 3 item 5 companion: SaveNav must round-trip the same
|
||||||
|
/// above-ceiling value back out unchanged (the save side never had a
|
||||||
|
/// clamp; this pins that save+load together preserve it).
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void JumpNodeSaveThenLoadRoundTripsChargeMillisecondsAboveRetailCeiling()
|
||||||
|
{
|
||||||
|
var source = new NavigationSettings { Mode = RouteMode.Once };
|
||||||
|
source.Waypoints.Add(new RouteWaypoint
|
||||||
|
{
|
||||||
|
Type = RouteWaypointType.Jump,
|
||||||
|
JumpHeadingDegrees = 90f,
|
||||||
|
JumpRun = true,
|
||||||
|
JumpChargeMilliseconds = 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
string af = MetafSerializer.SaveNav(source);
|
||||||
|
|
||||||
|
var target = new NavigationSettings();
|
||||||
|
Assert.True(MetafSerializer.TryLoadNav(af, target, NoOpSpellCatalog.Instance, out string error), error);
|
||||||
|
RouteWaypoint waypoint = Assert.Single(target.Waypoints);
|
||||||
|
Assert.Equal(5000, waypoint.JumpChargeMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertProfilesEqual(MetaProfile expected, MetaProfile actual)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected.Rules.Count, actual.Rules.Count);
|
||||||
|
for (int i = 0; i < expected.Rules.Count; i++)
|
||||||
|
{
|
||||||
|
MetaRule a = expected.Rules[i];
|
||||||
|
MetaRule b = actual.Rules[i];
|
||||||
|
Assert.Equal(a.State, b.State);
|
||||||
|
AssertConditionsEqual(a.Condition, b.Condition);
|
||||||
|
AssertActionsEqual(a.Action, b.Action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertConditionsEqual(MetaCondition expected, MetaCondition actual)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected.Kind, actual.Kind);
|
||||||
|
Assert.Equal(expected.Text, actual.Text);
|
||||||
|
Assert.Equal(expected.SecondaryText, actual.SecondaryText);
|
||||||
|
Assert.Equal(expected.Number, actual.Number, 6);
|
||||||
|
Assert.Equal(expected.SecondaryNumber, actual.SecondaryNumber, 6);
|
||||||
|
Assert.Equal(expected.TertiaryNumber, actual.TertiaryNumber, 6);
|
||||||
|
Assert.Equal(expected.Children.Count, actual.Children.Count);
|
||||||
|
for (int i = 0; i < expected.Children.Count; i++)
|
||||||
|
AssertConditionsEqual(expected.Children[i], actual.Children[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertActionsEqual(MetaAction expected, MetaAction actual)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected.Kind, actual.Kind);
|
||||||
|
if (expected.Kind == MetaActionKind.LoadEmbeddedNavigationRoute)
|
||||||
|
{
|
||||||
|
// Item B: MetaAction carries a typed EmbeddedRoute, not a
|
||||||
|
// re-parse-me blob in Text (which is now always empty for this
|
||||||
|
// action kind) — compare the routes waypoint-by-waypoint
|
||||||
|
// instead of skipping the comparison entirely.
|
||||||
|
Assert.Equal(expected.Text, actual.Text);
|
||||||
|
Assert.NotNull(expected.EmbeddedRoute);
|
||||||
|
Assert.NotNull(actual.EmbeddedRoute);
|
||||||
|
AssertNavigationEqual(expected.EmbeddedRoute!, actual.EmbeddedRoute!);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Equal(expected.Text, actual.Text);
|
||||||
|
}
|
||||||
|
Assert.Equal(expected.SecondaryText, actual.SecondaryText);
|
||||||
|
Assert.Equal(expected.Number, actual.Number, 6);
|
||||||
|
Assert.Equal(expected.SecondaryNumber, actual.SecondaryNumber, 6);
|
||||||
|
Assert.Equal(expected.Children.Count, actual.Children.Count);
|
||||||
|
for (int i = 0; i < expected.Children.Count; i++)
|
||||||
|
AssertActionsEqual(expected.Children[i], actual.Children[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertNavigationEqual(NavigationSettings expected, NavigationSettings actual)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected.Mode, actual.Mode);
|
||||||
|
if (expected.Mode == RouteMode.Target)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected.FollowTargetObjectId, actual.FollowTargetObjectId);
|
||||||
|
Assert.Equal(expected.FollowTargetName, actual.FollowTargetName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Assert.Equal(expected.Waypoints.Count, actual.Waypoints.Count);
|
||||||
|
for (int i = 0; i < expected.Waypoints.Count; i++)
|
||||||
|
{
|
||||||
|
RouteWaypoint a = expected.Waypoints[i];
|
||||||
|
RouteWaypoint b = actual.Waypoints[i];
|
||||||
|
Assert.Equal(a.Type, b.Type);
|
||||||
|
Assert.Equal(a.Position.EastWest, b.Position.EastWest, 3);
|
||||||
|
Assert.Equal(a.Position.NorthSouth, b.Position.NorthSouth, 3);
|
||||||
|
Assert.Equal(a.Position.Elevation, b.Position.Elevation, 3);
|
||||||
|
Assert.Equal(a.ReferencePosition.EastWest, b.ReferencePosition.EastWest, 3);
|
||||||
|
Assert.Equal(a.ReferencePosition.NorthSouth, b.ReferencePosition.NorthSouth, 3);
|
||||||
|
Assert.Equal(a.ReferencePosition.Elevation, b.ReferencePosition.Elevation, 3);
|
||||||
|
Assert.Equal(a.ObjectId, b.ObjectId);
|
||||||
|
Assert.Equal(a.ObjectName, b.ObjectName);
|
||||||
|
Assert.Equal(a.Text, b.Text);
|
||||||
|
Assert.Equal(a.DurationMilliseconds, b.DurationMilliseconds);
|
||||||
|
// Recall waypoints are the one field-set that is genuinely
|
||||||
|
// asymmetric between the two formats with a no-op spell
|
||||||
|
// catalog: the binary .nav stores a spell ID and derives the
|
||||||
|
// name only via a successful catalog lookup (which NoOpSpellCatalog
|
||||||
|
// never provides), while .af stores the name literally and
|
||||||
|
// derives the ID the same (catalog-dependent) way in reverse.
|
||||||
|
// Comparing RecallSpellName/Id here would fail for reasons that
|
||||||
|
// have nothing to do with the .af port's correctness.
|
||||||
|
Assert.Equal(a.JumpHeadingDegrees, b.JumpHeadingDegrees, 3);
|
||||||
|
Assert.Equal(a.JumpRun, b.JumpRun);
|
||||||
|
Assert.Equal(a.JumpChargeMilliseconds, b.JumpChargeMilliseconds);
|
||||||
|
// JumpDirection is likewise NOT compared here: metaf's .af
|
||||||
|
// format has no field for it at all (docs/research/vtank-kb/
|
||||||
|
// 06-navigation-and-nav.md section 6, gap 9), so a binary .nav
|
||||||
|
// fixture using StrafeLeft/StrafeRight always comes back
|
||||||
|
// Forward from the matching .af conversion — a real, expected
|
||||||
|
// asymmetry, not a bug in either reader.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NoOpSpellCatalog : ISpellCatalog
|
||||||
|
{
|
||||||
|
public static NoOpSpellCatalog Instance { get; } = new();
|
||||||
|
public IReadOnlyList<PluginSpellInfo> KnownSelfBuffs => [];
|
||||||
|
public bool TryGet(uint spellId, out PluginSpellInfo info)
|
||||||
|
{
|
||||||
|
info = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -164,7 +164,9 @@ public sealed class MossTankMarkupContractTests
|
||||||
XElement[] controls = root.Descendants()
|
XElement[] controls = root.Descendants()
|
||||||
.Where(element => interactive.Contains(element.Name.LocalName))
|
.Where(element => interactive.Contains(element.Name.LocalName))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
Assert.Equal(190, controls.Length);
|
// Round 3 item 10: +3 for the Route/Loot/Meta Delete buttons
|
||||||
|
// (Settings already had one from round 2 step 5).
|
||||||
|
Assert.Equal(194, controls.Length);
|
||||||
|
|
||||||
foreach (XElement control in controls)
|
foreach (XElement control in controls)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,401 @@ public sealed class MossTankPanelTests
|
||||||
Assert.Contains("{ this is not json", backup.Value, StringComparison.Ordinal);
|
Assert.Contains("{ this is not json", backup.Value, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round 3 item 12: a side-car monster rule whose Expression fails to
|
||||||
|
/// compile used to be silently dropped (a bare
|
||||||
|
/// <c>catch (FormatException) { }</c>); it must now log a warning
|
||||||
|
/// instead, while still falling back gracefully (the malformed rule is
|
||||||
|
/// skipped, everything else loads).
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void CorruptSideCarMonsterRuleIsLoggedNotSilentlySwallowed()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
string usdKey = VtankProfileDirectory.AutoCharacterFileName("Barris", string.Empty, "usd");
|
||||||
|
storage.Text[usdKey] = VtankDefaultSettingsDatabase.Parse().Render();
|
||||||
|
storage.Text["profiles/macro/sidecar/--Barris_.usd.json"] = """
|
||||||
|
{
|
||||||
|
"CombatRules": [
|
||||||
|
{ "Expression": "(((" },
|
||||||
|
{ "Expression": "DEFAULT" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var host = new FakeHost(new FakeAutomation { Name = "Barris" }, storage);
|
||||||
|
_ = new MossTankPanel(host);
|
||||||
|
|
||||||
|
Assert.Contains(host.Logger.Warnings, message =>
|
||||||
|
message.Contains("monster rule", StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reproduces the pre-cutover hashed JSON key a "By char" macro profile
|
||||||
|
/// used to be stored under (<c>MossTankProfileStore.LegacyProfileKey</c>,
|
||||||
|
/// not directly callable from a test — the hash and identity string are
|
||||||
|
/// reproduced verbatim here since the format is the migration contract
|
||||||
|
/// itself, byte for byte).
|
||||||
|
/// </summary>
|
||||||
|
private static string LegacyByCharacterProfileKey(string characterName)
|
||||||
|
{
|
||||||
|
string identity = "char:" + characterName.Trim().ToUpperInvariant();
|
||||||
|
string hash = Convert.ToHexString(
|
||||||
|
System.Security.Cryptography.SHA256.HashData(
|
||||||
|
System.Text.Encoding.UTF8.GetBytes(identity)));
|
||||||
|
return $"profiles/macro/{hash}.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FirstLoadMigratesLegacyJsonMacroProfileToUsdAndDeletesTheJsonKey()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
var automation = new FakeAutomation { Name = "Barris" };
|
||||||
|
string legacyKey = LegacyByCharacterProfileKey("Barris");
|
||||||
|
storage.Text[legacyKey] = """
|
||||||
|
{
|
||||||
|
"Combat": { "MaximumRange": 48.0 },
|
||||||
|
"ItemNames": ["Wand of Testing"]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var panel = new MossTankPanel(new FakeHost(automation, storage));
|
||||||
|
|
||||||
|
// The legacy key is gone; the real .usd file (this character's
|
||||||
|
// auto "By char" name) exists with the migrated value.
|
||||||
|
Assert.False(storage.Text.ContainsKey(legacyKey));
|
||||||
|
string usdKey = VtankProfileDirectory.AutoCharacterFileName("Barris", string.Empty, "usd");
|
||||||
|
Assert.True(storage.Text.ContainsKey(usdKey));
|
||||||
|
Assert.Equal(0.2d, panel.EvaluateExpression("uboptget['AttackDistance']").AsNumber(), precision: 7);
|
||||||
|
Assert.Contains("Wand of Testing", panel.ItemProfileText, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// Idempotent second run: a fresh panel against the same storage
|
||||||
|
// loads straight from the now-real .usd file, migrates nothing
|
||||||
|
// (there is no legacy key left to consult), and keeps the value.
|
||||||
|
var reloaded = new MossTankPanel(new FakeHost(
|
||||||
|
new FakeAutomation { Name = "Barris" }, storage));
|
||||||
|
Assert.Equal(0.2d, reloaded.EvaluateExpression("uboptget['AttackDistance']").AsNumber(), precision: 7);
|
||||||
|
Assert.Contains("Wand of Testing", reloaded.ItemProfileText, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExistingUsdCounterpartLeavesLegacyJsonUntouchedAndUnread()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
var automation = new FakeAutomation { Name = "Barris" };
|
||||||
|
string legacyKey = LegacyByCharacterProfileKey("Barris");
|
||||||
|
storage.Text[legacyKey] = """{ "Combat": { "MaximumRange": 240.0 } }""";
|
||||||
|
string usdKey = VtankProfileDirectory.AutoCharacterFileName("Barris", string.Empty, "usd");
|
||||||
|
var seedCombat = new CombatSettings { MaximumRange = 120d };
|
||||||
|
VtankDatabase seedDatabase = VtankSettingsProfileSerializer.CreateNew(
|
||||||
|
new VtankSettingsProfileSerializer.AllSettings
|
||||||
|
{
|
||||||
|
Combat = seedCombat,
|
||||||
|
Buffs = new BuffSettings(),
|
||||||
|
Vitals = new VitalSettings(),
|
||||||
|
Inventory = new InventorySettings(),
|
||||||
|
Navigation = new NavigationSettings(),
|
||||||
|
});
|
||||||
|
storage.Text[usdKey] = seedDatabase.Render();
|
||||||
|
|
||||||
|
var panel = new MossTankPanel(new FakeHost(automation, storage));
|
||||||
|
|
||||||
|
// The real .usd file wins; the stale legacy JSON is left completely
|
||||||
|
// alone (not read, not deleted) rather than silently discarded.
|
||||||
|
Assert.True(storage.Text.ContainsKey(legacyKey));
|
||||||
|
Assert.Equal(0.5d, panel.EvaluateExpression("uboptget['AttackDistance']").AsNumber(), precision: 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round 3 item 1 (BLOCKER): a drop-in <c>.usd</c> with
|
||||||
|
/// <c>EnableLooting=True</c> and NO side-car at all must load with
|
||||||
|
/// looting enabled. Before the fix, <c>SideCarDocument.Apply</c> ran
|
||||||
|
/// AFTER the <c>.usd</c> and unconditionally wrote its own
|
||||||
|
/// <c>InventoryEnableLooting</c> field (defaulting to <see langword="false"/>
|
||||||
|
/// for a profile that has never had a side-car written) over
|
||||||
|
/// <c>settings.Inventory.Loot.Enabled</c>, silently turning looting back
|
||||||
|
/// off.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void DropInUsdWithLootingEnabledAndNoSideCarLoadsLootingEnabled()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
var automation = new FakeAutomation { Name = "Barris" };
|
||||||
|
string usdKey = VtankProfileDirectory.AutoCharacterFileName("Barris", string.Empty, "usd");
|
||||||
|
|
||||||
|
VtankDatabase database = VtankDefaultSettingsDatabase.Parse();
|
||||||
|
VtankTable settingsTable = database.Find("Settings")!;
|
||||||
|
int nameColumn = settingsTable.ColumnIndex("Setting");
|
||||||
|
int valueColumn = settingsTable.ColumnIndex("Value");
|
||||||
|
VtankRow row = settingsTable.Rows.First(candidate =>
|
||||||
|
candidate.Cells[nameColumn].AsString().Equals(
|
||||||
|
"EnableLooting", StringComparison.OrdinalIgnoreCase));
|
||||||
|
row.Cells[valueColumn] = VtankCell.Bool(true);
|
||||||
|
storage.Text[usdKey] = database.Render();
|
||||||
|
|
||||||
|
// No side-car key at all — a real drop-in, not something MossTank
|
||||||
|
// itself ever saved.
|
||||||
|
var panel = new MossTankPanel(new FakeHost(automation, storage));
|
||||||
|
|
||||||
|
Assert.True(panel.LootEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round 3 item 2: the pre-cutover "profiles/index.json" roster carried
|
||||||
|
/// EVERY named profile a character had (<c>Profiles: [{Name, Owner}]</c>),
|
||||||
|
/// not just whichever one happened to be selected. Before the fix, the
|
||||||
|
/// settings cutover only ever converted the currently-selected profile
|
||||||
|
/// (via <c>MigrateLegacyIfNeeded</c>) — every OTHER named profile in the
|
||||||
|
/// roster was silently orphaned: never converted, never listed, and (once
|
||||||
|
/// <c>SetMineOnly</c> saved) permanently unreachable once the shared key
|
||||||
|
/// got flattened to a bare <c>{MineOnly}</c> document.
|
||||||
|
/// </summary>
|
||||||
|
private static string LegacyNamedProfileKey(string name)
|
||||||
|
{
|
||||||
|
string identity = "named:" + name.Trim().ToUpperInvariant();
|
||||||
|
string hash = Convert.ToHexString(
|
||||||
|
System.Security.Cryptography.SHA256.HashData(
|
||||||
|
System.Text.Encoding.UTF8.GetBytes(identity)));
|
||||||
|
return $"profiles/macro/{hash}.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SettingsRosterSweepConvertsEveryNamedLegacyProfileOnce()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
var automation = new FakeAutomation { Name = "Barris" };
|
||||||
|
storage.Text["profiles/index.json"] = """
|
||||||
|
{
|
||||||
|
"Version": 1,
|
||||||
|
"MineOnly": false,
|
||||||
|
"Profiles": [
|
||||||
|
{ "Name": "Farming", "Owner": "Barris" },
|
||||||
|
{ "Name": "Buffing", "Owner": "Barris" }
|
||||||
|
],
|
||||||
|
"SelectedByCharacter": {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
storage.Text[LegacyNamedProfileKey("Farming")] =
|
||||||
|
"""{ "Combat": { "MaximumRange": 42.0 } }""";
|
||||||
|
storage.Text[LegacyNamedProfileKey("Buffing")] =
|
||||||
|
"""{ "Combat": { "MaximumRange": 24.0 } }""";
|
||||||
|
|
||||||
|
var panel = new MossTankPanel(new FakeHost(automation, storage));
|
||||||
|
|
||||||
|
// MineOnly recovered from the old roster's own shape...
|
||||||
|
Assert.False(panel.MineOnlyEnabled);
|
||||||
|
// ...and both named profiles converted to real per-character
|
||||||
|
// sub-profile .usd files, each keeping its own distinct value.
|
||||||
|
string farmingUsd = VtankProfileDirectory.SubProfilePrefix("Barris", string.Empty)
|
||||||
|
+ "Farming.usd";
|
||||||
|
string buffingUsd = VtankProfileDirectory.SubProfilePrefix("Barris", string.Empty)
|
||||||
|
+ "Buffing.usd";
|
||||||
|
Assert.Equal(42d, RangeOf(storage, farmingUsd));
|
||||||
|
Assert.Equal(24d, RangeOf(storage, buffingUsd));
|
||||||
|
|
||||||
|
// Both legacy JSON keys and the whole (now fully-swept) roster key
|
||||||
|
// are gone; MineOnly now lives at its own dedicated key.
|
||||||
|
Assert.False(storage.Text.ContainsKey(LegacyNamedProfileKey("Farming")));
|
||||||
|
Assert.False(storage.Text.ContainsKey(LegacyNamedProfileKey("Buffing")));
|
||||||
|
Assert.False(storage.Text.ContainsKey("profiles/index.json"));
|
||||||
|
Assert.True(storage.Text.ContainsKey("profiles/macro/preferences.json"));
|
||||||
|
|
||||||
|
// Idempotent: a fresh panel against the same storage sweeps nothing
|
||||||
|
// more (there is no roster key left to read) and keeps both values.
|
||||||
|
var reloaded = new MossTankPanel(new FakeHost(
|
||||||
|
new FakeAutomation { Name = "Barris" }, storage));
|
||||||
|
Assert.False(reloaded.MineOnlyEnabled);
|
||||||
|
Assert.Equal(42d, RangeOf(storage, farmingUsd));
|
||||||
|
Assert.Equal(24d, RangeOf(storage, buffingUsd));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double RangeOf(MemoryStorage storage, string usdKey)
|
||||||
|
{
|
||||||
|
string text = Assert.Contains(usdKey, (IDictionary<string, string>)storage.Text);
|
||||||
|
var settings = new VtankSettingsProfileSerializer.AllSettings
|
||||||
|
{
|
||||||
|
Combat = new CombatSettings(),
|
||||||
|
Buffs = new BuffSettings(),
|
||||||
|
Vitals = new VitalSettings(),
|
||||||
|
Inventory = new InventorySettings(),
|
||||||
|
Navigation = new NavigationSettings(),
|
||||||
|
};
|
||||||
|
VtankSettingsProfileSerializer.Load(text, settings);
|
||||||
|
return settings.Combat.MaximumRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reproduces MossTankMetaProfileStore's pre-cutover by-character JSON
|
||||||
|
/// hash key (its own <c>Hash(string)</c> — 12-byte truncated SHA256,
|
||||||
|
/// lowercase hex — is private; the format is the migration contract
|
||||||
|
/// itself, reproduced verbatim here).
|
||||||
|
/// </summary>
|
||||||
|
private static string LegacyMetaByCharacterKey(string characterName)
|
||||||
|
{
|
||||||
|
byte[] hash = System.Security.Cryptography.SHA256.HashData(
|
||||||
|
System.Text.Encoding.UTF8.GetBytes(characterName.ToLowerInvariant()));
|
||||||
|
return $"profiles/meta/by-character/{Convert.ToHexString(hash.AsSpan(0, 12)).ToLowerInvariant()}.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MetaStoreMigratesLegacyJsonProfileToAfAndDeletesTheJsonKey()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
string legacyKey = LegacyMetaByCharacterKey("Barris");
|
||||||
|
var legacyProfile = new MetaProfile
|
||||||
|
{
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new MetaRule
|
||||||
|
{
|
||||||
|
State = "Default",
|
||||||
|
Condition = MetaCondition.Always(),
|
||||||
|
Action = new MetaAction { Kind = MetaActionKind.ChatCommand, Text = "/say hi" },
|
||||||
|
Enabled = true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
storage.Text[legacyKey] = System.Text.Json.JsonSerializer.Serialize(
|
||||||
|
legacyProfile,
|
||||||
|
new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
|
||||||
|
|
||||||
|
var store = new MossTankMetaProfileStore(
|
||||||
|
new FakeHost(new FakeAutomation { Name = "Barris" }, storage));
|
||||||
|
store.BindCharacter("Barris");
|
||||||
|
MetaProfile loaded = store.LoadCurrent();
|
||||||
|
|
||||||
|
Assert.False(storage.Text.ContainsKey(legacyKey));
|
||||||
|
MetaRule rule = Assert.Single(loaded.Rules);
|
||||||
|
Assert.Equal("/say hi", rule.Action.Text);
|
||||||
|
|
||||||
|
// Idempotent second run: nothing left to migrate, loads straight
|
||||||
|
// from the now-real .af file.
|
||||||
|
var reopened = new MossTankMetaProfileStore(
|
||||||
|
new FakeHost(new FakeAutomation { Name = "Barris" }, storage));
|
||||||
|
reopened.BindCharacter("Barris");
|
||||||
|
MetaProfile reloaded = reopened.LoadCurrent();
|
||||||
|
Assert.Single(reloaded.Rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MetaStoreLeavesLegacyJsonUntouchedWhenAfCounterpartExists()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
string legacyKey = LegacyMetaByCharacterKey("Barris");
|
||||||
|
storage.Text[legacyKey] = System.Text.Json.JsonSerializer.Serialize(new MetaProfile
|
||||||
|
{
|
||||||
|
Rules = [new MetaRule { Action = new MetaAction { Kind = MetaActionKind.ChatCommand, Text = "/say stale" } }],
|
||||||
|
});
|
||||||
|
string realKey = VtankProfileDirectory.AutoCharacterFileName("Barris", string.Empty, "af");
|
||||||
|
storage.Text[realKey] = MetafSerializer.SaveMeta(new MetaProfile
|
||||||
|
{
|
||||||
|
Rules = [new MetaRule { Action = new MetaAction { Kind = MetaActionKind.ChatCommand, Text = "/say real" } }],
|
||||||
|
});
|
||||||
|
|
||||||
|
var store = new MossTankMetaProfileStore(
|
||||||
|
new FakeHost(new FakeAutomation { Name = "Barris" }, storage));
|
||||||
|
store.BindCharacter("Barris");
|
||||||
|
MetaProfile loaded = store.LoadCurrent();
|
||||||
|
|
||||||
|
Assert.True(storage.Text.ContainsKey(legacyKey));
|
||||||
|
Assert.Equal("/say real", Assert.Single(loaded.Rules).Action.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reproduces MossTankMetaProfileStore's pre-cutover named-profile JSON
|
||||||
|
/// hash key (its own <c>LegacyNamedKey</c>/<c>Hash(string)</c> are
|
||||||
|
/// private; the format is the migration contract itself, reproduced
|
||||||
|
/// verbatim here).
|
||||||
|
/// </summary>
|
||||||
|
private static string LegacyMetaNamedKey(string name)
|
||||||
|
{
|
||||||
|
byte[] hash = System.Security.Cryptography.SHA256.HashData(
|
||||||
|
System.Text.Encoding.UTF8.GetBytes(name.ToLowerInvariant()));
|
||||||
|
return $"profiles/meta/named/{Convert.ToHexString(hash.AsSpan(0, 12)).ToLowerInvariant()}.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round 3 item 2: like the settings roster, MossTankMetaProfileStore's
|
||||||
|
/// pre-cutover "profiles/meta/index.json" carried EVERY named Meta
|
||||||
|
/// profile's name (never owner-scoped — one shared, globally-hashed key
|
||||||
|
/// per name), and the cutover stopped reading it entirely — orphaning
|
||||||
|
/// every named profile except whichever one happened to be selected.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void MetaRosterSweepConvertsEveryNamedLegacyProfileOnce()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
storage.Text["profiles/meta/index.json"] = """{ "Names": ["Farming", "Buffing"] }""";
|
||||||
|
storage.Text[LegacyMetaNamedKey("Farming")] = System.Text.Json.JsonSerializer.Serialize(
|
||||||
|
new MetaProfile
|
||||||
|
{
|
||||||
|
Rules = [new MetaRule { Action = new MetaAction { Kind = MetaActionKind.ChatCommand, Text = "/say farming" } }],
|
||||||
|
});
|
||||||
|
storage.Text[LegacyMetaNamedKey("Buffing")] = System.Text.Json.JsonSerializer.Serialize(
|
||||||
|
new MetaProfile
|
||||||
|
{
|
||||||
|
Rules = [new MetaRule { Action = new MetaAction { Kind = MetaActionKind.ChatCommand, Text = "/say buffing" } }],
|
||||||
|
});
|
||||||
|
|
||||||
|
var store = new MossTankMetaProfileStore(
|
||||||
|
new FakeHost(new FakeAutomation { Name = "Barris" }, storage));
|
||||||
|
store.BindCharacter("Barris");
|
||||||
|
store.LoadCurrent();
|
||||||
|
|
||||||
|
Assert.False(storage.Text.ContainsKey(LegacyMetaNamedKey("Farming")));
|
||||||
|
Assert.False(storage.Text.ContainsKey(LegacyMetaNamedKey("Buffing")));
|
||||||
|
Assert.False(storage.Text.ContainsKey("profiles/meta/index.json"));
|
||||||
|
Assert.True(MetafSerializer.TryLoadMeta(
|
||||||
|
storage.Text["Farming.af"], NoOpSpellCatalogForExport.Instance, out MetaProfile farming, out _));
|
||||||
|
Assert.Equal("/say farming", Assert.Single(farming.Rules).Action.Text);
|
||||||
|
Assert.True(MetafSerializer.TryLoadMeta(
|
||||||
|
storage.Text["Buffing.af"], NoOpSpellCatalogForExport.Instance, out MetaProfile buffing, out _));
|
||||||
|
Assert.Equal("/say buffing", Assert.Single(buffing.Rules).Action.Text);
|
||||||
|
|
||||||
|
// Idempotent: a fresh store against the same storage sweeps nothing
|
||||||
|
// more (there is no roster key left to read).
|
||||||
|
var reopened = new MossTankMetaProfileStore(
|
||||||
|
new FakeHost(new FakeAutomation { Name = "Barris" }, storage));
|
||||||
|
reopened.BindCharacter("Barris");
|
||||||
|
reopened.LoadCurrent();
|
||||||
|
Assert.True(storage.Text.ContainsKey("Farming.af"));
|
||||||
|
Assert.True(storage.Text.ContainsKey("Buffing.af"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MetaStoreRefusesToSaveADisabledRuleAndKeepsThePriorAfContent()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
var store = new MossTankMetaProfileStore(
|
||||||
|
new FakeHost(new FakeAutomation { Name = "Barris" }, storage));
|
||||||
|
store.BindCharacter("Barris");
|
||||||
|
var enabledOnly = new MetaProfile
|
||||||
|
{
|
||||||
|
Rules = [new MetaRule { Action = new MetaAction { Kind = MetaActionKind.ChatCommand, Text = "/say good" } }],
|
||||||
|
};
|
||||||
|
Assert.True(store.SaveCurrent(enabledOnly));
|
||||||
|
string key = VtankProfileDirectory.AutoCharacterFileName("Barris", string.Empty, "af");
|
||||||
|
string goodContent = storage.Text[key];
|
||||||
|
|
||||||
|
var withDisabledRule = new MetaProfile
|
||||||
|
{
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new MetaRule { Action = new MetaAction { Kind = MetaActionKind.ChatCommand, Text = "/say good" } },
|
||||||
|
new MetaRule { Action = new MetaAction { Kind = MetaActionKind.ChatCommand, Text = "/say off" }, Enabled = false },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
bool saved = store.SaveCurrent(withDisabledRule);
|
||||||
|
|
||||||
|
Assert.False(saved);
|
||||||
|
Assert.NotNull(store.SaveNotice);
|
||||||
|
Assert.Contains("disabled", store.SaveNotice, StringComparison.OrdinalIgnoreCase);
|
||||||
|
// The refusal must not silently drop the disabled rule: the file on
|
||||||
|
// disk keeps its last good, fully-representable content.
|
||||||
|
Assert.Equal(goodContent, storage.Text[key]);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FirstRunGuidanceExplainsProfilesImportsAndPersistentShelfOnce()
|
public void FirstRunGuidanceExplainsProfilesImportsAndPersistentShelfOnce()
|
||||||
{
|
{
|
||||||
|
|
@ -477,7 +872,89 @@ public sealed class MossTankPanelTests
|
||||||
Assert.Contains("name ~= coin", second.LootRuleRows[0], StringComparison.Ordinal);
|
Assert.Contains("name ~= coin", second.LootRuleRows[0], StringComparison.Ordinal);
|
||||||
Assert.Equal("Keep up to 2", second.LootKeepCountText);
|
Assert.Equal("Keep up to 2", second.LootKeepCountText);
|
||||||
Assert.Equal("Priority 1", second.LootPriorityText);
|
Assert.Equal("Priority 1", second.LootPriorityText);
|
||||||
Assert.Equal("Corpse range 38m", second.LootRangeText);
|
// A brand-new profile now seeds from VTank's own shipped
|
||||||
|
// defaultsettings.usd (CorpseApproachRange-Max = 0), not a
|
||||||
|
// MossTank-invented 40m CLR default, so one decrement from 0 clamps
|
||||||
|
// at the 2m floor rather than landing on 38.
|
||||||
|
Assert.Equal("Corpse range 2m", second.LootRangeText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reproduces MossTankLootProfileStore's pre-cutover hashed JSON key
|
||||||
|
/// (its own private <c>ProfileKey</c> is not directly callable from a
|
||||||
|
/// test — the hash and identity string are reproduced verbatim here
|
||||||
|
/// since the format is the migration contract itself, byte for byte).
|
||||||
|
/// </summary>
|
||||||
|
private static string LegacyLootProfileKey(string value, bool byCharacter)
|
||||||
|
{
|
||||||
|
string identity = (byCharacter ? "char:" : "named:") + value.Trim().ToUpperInvariant();
|
||||||
|
string hash = Convert.ToHexString(
|
||||||
|
System.Security.Cryptography.SHA256.HashData(
|
||||||
|
System.Text.Encoding.UTF8.GetBytes(identity)));
|
||||||
|
return $"profiles/loot/{hash}.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round 3 item 9: unlike Meta/Route (whose pre-cutover roster was
|
||||||
|
/// already abandoned before their own cutovers), loot's
|
||||||
|
/// "profiles/loot/index.json" was still the LIVE mechanism right up to
|
||||||
|
/// this exact commit, so this single one-time sweep must convert BOTH
|
||||||
|
/// this character's own "By char" JSON document AND every other named
|
||||||
|
/// profile the roster still lists — not just whichever one happened to
|
||||||
|
/// be selected.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void LootRosterSweepConvertsByCharacterAndEveryNamedLegacyProfileOnce()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
var automation = new FakeAutomation { Name = "Barris" };
|
||||||
|
storage.Text[LegacyLootProfileKey("Barris", byCharacter: true)] = """
|
||||||
|
{
|
||||||
|
"Rules": [
|
||||||
|
{ "Name": "Coins", "Expression": "name ~= coin", "Action": 0, "Priority": 3 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
storage.Text["profiles/loot/index.json"] = """
|
||||||
|
{
|
||||||
|
"Version": 1,
|
||||||
|
"Names": ["Farming"],
|
||||||
|
"SelectedByCharacter": {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
storage.Text[LegacyLootProfileKey("Farming", byCharacter: false)] = """
|
||||||
|
{
|
||||||
|
"Rules": [
|
||||||
|
{ "Name": "Salvage", "Expression": "name ~= salvage", "Action": 0, "Priority": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var panel = new MossTankPanel(new FakeHost(automation, storage));
|
||||||
|
|
||||||
|
string byCharacterFile = VtankProfileDirectory.AutoCharacterFileName(
|
||||||
|
"Barris", string.Empty, "utl");
|
||||||
|
Assert.True(storage.Text.ContainsKey(byCharacterFile));
|
||||||
|
Assert.True(storage.Text.ContainsKey("Farming.utl"));
|
||||||
|
Assert.False(storage.Text.ContainsKey(LegacyLootProfileKey("Barris", byCharacter: true)));
|
||||||
|
Assert.False(storage.Text.ContainsKey(LegacyLootProfileKey("Farming", byCharacter: false)));
|
||||||
|
Assert.False(storage.Text.ContainsKey("profiles/loot/index.json"));
|
||||||
|
|
||||||
|
// The "By char" profile is already loaded (panel construction binds
|
||||||
|
// and loads it); the raw .utl round-trips VTClassic-side, but the
|
||||||
|
// human expression text is only visible through the store's own
|
||||||
|
// load path (MossTankLootProfileStore.ApplyMossTankExpressions),
|
||||||
|
// which the panel's rule rows expose.
|
||||||
|
Assert.Contains("name ~= coin", Assert.Single(panel.LootRuleRows), StringComparison.Ordinal);
|
||||||
|
|
||||||
|
panel.SelectLootProfile("Farming");
|
||||||
|
Assert.Contains("name ~= salvage", Assert.Single(panel.LootRuleRows), StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// Idempotent: a fresh panel against the same storage sweeps nothing
|
||||||
|
// more (there is no roster key left to read) and keeps both values.
|
||||||
|
var reloaded = new MossTankPanel(new FakeHost(
|
||||||
|
new FakeAutomation { Name = "Barris" }, storage));
|
||||||
|
Assert.Contains("name ~= coin", Assert.Single(reloaded.LootRuleRows), StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -500,9 +977,9 @@ public sealed class MossTankPanelTests
|
||||||
Assert.Single(panel.LootRuleRows);
|
Assert.Single(panel.LootRuleRows);
|
||||||
panel.SelectLootProfile("Currency");
|
panel.SelectLootProfile("Currency");
|
||||||
Assert.Equal(2, panel.LootRuleRows.Count);
|
Assert.Equal(2, panel.LootRuleRows.Count);
|
||||||
Assert.Contains(
|
// Round 3 item 9: a named loot profile is now a real .utl file, not
|
||||||
storage.Text.Keys,
|
// a hashed JSON document under profiles/loot/.
|
||||||
key => key.StartsWith("profiles/loot/", StringComparison.Ordinal));
|
Assert.True(storage.Text.ContainsKey("Currency.utl"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -618,7 +1095,10 @@ public sealed class MossTankPanelTests
|
||||||
Assert.Equal("Legacy", panel.LootProfileName);
|
Assert.Equal("Legacy", panel.LootProfileName);
|
||||||
Assert.Single(panel.LootRuleRows);
|
Assert.Single(panel.LootRuleRows);
|
||||||
Assert.Contains("KeepUpTo", panel.LootRuleRows[0], StringComparison.Ordinal);
|
Assert.Contains("KeepUpTo", panel.LootRuleRows[0], StringComparison.Ordinal);
|
||||||
string exported = storage.Text["exports/Legacy.utl"];
|
// Round 3 item 9: the imported profile is now written directly as
|
||||||
|
// the real "Legacy.utl" file — there is no separate exports/
|
||||||
|
// mirror any more (WriteLegacyExport is deleted).
|
||||||
|
string exported = storage.Text["Legacy.utl"];
|
||||||
Assert.True(VtankLootProfileSerializer.TryRead(
|
Assert.True(VtankLootProfileSerializer.TryRead(
|
||||||
exported,
|
exported,
|
||||||
out VtankLootProfile roundTrip,
|
out VtankLootProfile roundTrip,
|
||||||
|
|
@ -638,18 +1118,158 @@ public sealed class MossTankPanelTests
|
||||||
panel.SetProfileNameDraft("Fellowship");
|
panel.SetProfileNameDraft("Fellowship");
|
||||||
panel.CopyProfile();
|
panel.CopyProfile();
|
||||||
|
|
||||||
Assert.Equal("Fellowship", panel.SelectedMacroProfile);
|
// Selection now surfaces the real VTank file name (the
|
||||||
Assert.Contains("Fellowship", panel.MacroProfileNames);
|
// "--Name_Server_" sub-profile convention), not the bare name the
|
||||||
|
// user typed — this is the whole point of the .usd cutover.
|
||||||
|
const string fellowshipFile = "--Moss Wart__Fellowship.usd";
|
||||||
|
Assert.Equal(fellowshipFile, panel.SelectedMacroProfile);
|
||||||
|
Assert.Contains(fellowshipFile, panel.MacroProfileNames);
|
||||||
panel.SetNormalHealth(0.88f);
|
panel.SetNormalHealth(0.88f);
|
||||||
panel.SelectMacroProfile(MossTankProfileStore.ByCharacter);
|
panel.SelectMacroProfile(MossTankProfileStore.ByCharacter);
|
||||||
|
|
||||||
Assert.Equal(0.42f, panel.NormalHealthValue, precision: 2);
|
Assert.Equal(0.42f, panel.NormalHealthValue, precision: 2);
|
||||||
|
// A bare name typed back (matching VTank's own "/vt settings load"
|
||||||
|
// convention) still resolves to this character's own sub-profile.
|
||||||
panel.SelectMacroProfile("Fellowship");
|
panel.SelectMacroProfile("Fellowship");
|
||||||
|
Assert.Equal(fellowshipFile, panel.SelectedMacroProfile);
|
||||||
Assert.Equal(0.88f, panel.NormalHealthValue, precision: 2);
|
Assert.Equal(0.88f, panel.NormalHealthValue, precision: 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Campaign VT slice 1 Part A round 2 step 5: the Profiles tab's new
|
||||||
|
// Delete button (mosstank.xml), bound through DeleteProfile exactly
|
||||||
|
// like every other markup action.
|
||||||
[Fact]
|
[Fact]
|
||||||
public void NavCommandsImportAndExportExactVtankNavFiles()
|
public void DeleteProfileRemovesTheRealFileAndFallsBackToByCharacter()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
var automation = new FakeAutomation { Name = "Moss Wart" };
|
||||||
|
var panel = new MossTankPanel(new FakeHost(automation, storage));
|
||||||
|
panel.SetNormalHealth(0.42f);
|
||||||
|
panel.SetProfileNameDraft("Fellowship");
|
||||||
|
panel.CopyProfile();
|
||||||
|
const string fellowshipFile = "--Moss Wart__Fellowship.usd";
|
||||||
|
Assert.Equal(fellowshipFile, panel.SelectedMacroProfile);
|
||||||
|
Assert.Contains(fellowshipFile, panel.MacroProfileNames);
|
||||||
|
|
||||||
|
panel.DeleteProfile();
|
||||||
|
|
||||||
|
Assert.Equal(MossTankProfileStore.ByCharacter, panel.SelectedMacroProfile);
|
||||||
|
Assert.DoesNotContain(fellowshipFile, panel.MacroProfileNames);
|
||||||
|
Assert.False(storage.Text.ContainsKey(fellowshipFile));
|
||||||
|
Assert.Contains("Deleted", panel.ProfileLifecycleNotice, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeleteProfileRefusesToRemoveByCharacter()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
var panel = new MossTankPanel(new FakeHost(new FakeAutomation(), storage));
|
||||||
|
|
||||||
|
panel.DeleteProfile();
|
||||||
|
|
||||||
|
Assert.Equal(MossTankProfileStore.ByCharacter, panel.SelectedMacroProfile);
|
||||||
|
Assert.Contains("cannot be deleted", panel.ProfileLifecycleNotice, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round 3 item 10: Meta/Route/Loot get the same Delete action Settings
|
||||||
|
// already had from round 2 step 5.
|
||||||
|
[Fact]
|
||||||
|
public void DeleteRouteProfileRemovesTheRealFileAndFallsBackToByCharacter()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
var panel = new MossTankPanel(new FakeHost(new FakeAutomation(), storage));
|
||||||
|
panel.SetRouteProfileNameDraft("Fellowship");
|
||||||
|
panel.CopyRouteProfile();
|
||||||
|
Assert.Equal("Fellowship", panel.SelectedRouteProfile);
|
||||||
|
Assert.Contains("Fellowship", panel.RouteProfileNames);
|
||||||
|
|
||||||
|
panel.DeleteRouteProfile();
|
||||||
|
|
||||||
|
Assert.Equal(MossTankRouteProfileStore.ByCharacter, panel.SelectedRouteProfile);
|
||||||
|
Assert.DoesNotContain("Fellowship", panel.RouteProfileNames);
|
||||||
|
Assert.False(storage.Text.ContainsKey("nav_Fellowship.af"));
|
||||||
|
Assert.Contains("Deleted", panel.RouteNotice, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeleteRouteProfileRefusesToRemoveByCharacter()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
var panel = new MossTankPanel(new FakeHost(new FakeAutomation(), storage));
|
||||||
|
|
||||||
|
panel.DeleteRouteProfile();
|
||||||
|
|
||||||
|
Assert.Equal(MossTankRouteProfileStore.ByCharacter, panel.SelectedRouteProfile);
|
||||||
|
Assert.Contains("cannot be deleted", panel.RouteNotice, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeleteMetaProfileRemovesTheRealFileAndFallsBackToByCharacter()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
var panel = new MossTankPanel(new FakeHost(new FakeAutomation(), storage));
|
||||||
|
panel.SetMetaProfileNameDraft("Fellowship");
|
||||||
|
panel.CopyMetaProfile();
|
||||||
|
Assert.Equal("Fellowship", panel.SelectedMetaProfile);
|
||||||
|
Assert.Contains("Fellowship", panel.MetaProfileNames);
|
||||||
|
|
||||||
|
panel.DeleteMetaProfile();
|
||||||
|
|
||||||
|
Assert.Equal(MossTankMetaProfileStore.ByCharacter, panel.SelectedMetaProfile);
|
||||||
|
Assert.DoesNotContain("Fellowship", panel.MetaProfileNames);
|
||||||
|
Assert.False(storage.Text.ContainsKey("Fellowship.af"));
|
||||||
|
Assert.Contains("Deleted", panel.MetaNotice, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeleteMetaProfileRefusesToRemoveByCharacter()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
var panel = new MossTankPanel(new FakeHost(new FakeAutomation(), storage));
|
||||||
|
|
||||||
|
panel.DeleteMetaProfile();
|
||||||
|
|
||||||
|
Assert.Equal(MossTankMetaProfileStore.ByCharacter, panel.SelectedMetaProfile);
|
||||||
|
Assert.Contains("cannot be deleted", panel.MetaNotice, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeleteLootProfileRemovesTheRealFileAndFallsBackToByCharacter()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
var panel = new MossTankPanel(new FakeHost(new FakeAutomation(), storage));
|
||||||
|
panel.SetLootProfileNameDraft("Fellowship");
|
||||||
|
panel.CopyLootProfile();
|
||||||
|
Assert.Equal("Fellowship", panel.LootProfileName);
|
||||||
|
Assert.Contains("Fellowship", panel.LootProfileNames);
|
||||||
|
|
||||||
|
panel.DeleteLootProfile();
|
||||||
|
|
||||||
|
Assert.Equal(MossTankLootProfileStore.ByCharacter, panel.LootProfileName);
|
||||||
|
Assert.DoesNotContain("Fellowship", panel.LootProfileNames);
|
||||||
|
Assert.False(storage.Text.ContainsKey("Fellowship.utl"));
|
||||||
|
Assert.Contains("Deleted", panel.LootEditorNotice, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeleteLootProfileRefusesToRemoveByCharacter()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
var panel = new MossTankPanel(new FakeHost(new FakeAutomation(), storage));
|
||||||
|
|
||||||
|
panel.DeleteLootProfile();
|
||||||
|
|
||||||
|
Assert.Equal(MossTankLootProfileStore.ByCharacter, panel.LootProfileName);
|
||||||
|
Assert.Contains("cannot be deleted", panel.LootEditorNotice, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item J (slice-1 fix round): renamed from
|
||||||
|
// NavCommandsImportAndExportExactVtankNavFiles — .af is a real writer
|
||||||
|
// output now (item E's header/fold-marker emission), not a byte-exact
|
||||||
|
// pass-through of the imported .nav, so the old name overstated what
|
||||||
|
// this proves.
|
||||||
|
[Fact]
|
||||||
|
public void NavCommandsImportLegacyAndExportAf()
|
||||||
{
|
{
|
||||||
var storage = new MemoryStorage();
|
var storage = new MemoryStorage();
|
||||||
storage.Text["imports/Legacy.nav"] = """
|
storage.Text["imports/Legacy.nav"] = """
|
||||||
|
|
@ -672,34 +1292,89 @@ public sealed class MossTankPanelTests
|
||||||
Assert.Single(panel.RouteRows);
|
Assert.Single(panel.RouteRows);
|
||||||
Assert.Contains("12.5", panel.RouteRows[0], StringComparison.Ordinal);
|
Assert.Contains("12.5", panel.RouteRows[0], StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// .af is the ONLY storage/authoring format now (Campaign VT slice 1
|
||||||
|
// Part A round 2 — the old "exports/nav/" side-car mirror is gone;
|
||||||
|
// this is the real route file, named with the "nav_" prefix so it
|
||||||
|
// never collides with a Meta profile of the same user-typed name in
|
||||||
|
// the shared VtankProfiles directory).
|
||||||
Command(panel, "nav save Exported.nav");
|
Command(panel, "nav save Exported.nav");
|
||||||
Assert.StartsWith(
|
// Item E (slice-1 fix round): the writer now prepends metaf's own
|
||||||
"uTank2 NAV 1.2\r\n",
|
// navHeader block, so the file no longer STARTS with "NAV: " —
|
||||||
storage.Text["exports/Exported.nav"],
|
// Contains proves the body is still there.
|
||||||
|
Assert.Contains(
|
||||||
|
"NAV: ",
|
||||||
|
storage.Text["nav_Exported.af"],
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Item J (slice-1 fix round): "/vt nav save Foo.af" used to keep the
|
||||||
|
// ".af" suffix (only ".nav" was stripped), producing a doubled
|
||||||
|
// "nav_Foo.af.af" export instead of "nav_Foo.af".
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MetaCommandsImportAndExportExactVtankMetFiles()
|
public void NavSaveAcceptsAnAfSuffixedNameWithoutDoublingIt()
|
||||||
{
|
{
|
||||||
var storage = new MemoryStorage();
|
var storage = new MemoryStorage();
|
||||||
storage.Text["imports/Legacy.met"] = VtankMetaProfileSerializer.Save(
|
var panel = new MossTankPanel(new FakeHost(new FakeAutomation(), storage));
|
||||||
new MetaProfile
|
|
||||||
{
|
Command(panel, "nav save Foo.af");
|
||||||
Rules =
|
|
||||||
[
|
Assert.True(storage.Text.ContainsKey("nav_Foo.af"));
|
||||||
new MetaRule
|
Assert.False(storage.Text.ContainsKey("nav_Foo.af.af"));
|
||||||
{
|
}
|
||||||
State = "Default",
|
|
||||||
Condition = MetaCondition.Always(),
|
// Item C (Campaign VT slice-1 fix round): a Meta profile and a route
|
||||||
Action = new MetaAction
|
// (Navigation) profile named identically used to write into the SAME
|
||||||
{
|
// flat "exports/" directory — "Same.af" from one silently clobbered
|
||||||
Kind = MetaActionKind.ChatCommand,
|
// "Same.af" from the other. Campaign VT slice-1 round 2 replaced the
|
||||||
Text = "/say imported",
|
// separate exports/meta/ + exports/nav/ side-car mirrors with the real
|
||||||
},
|
// storage cutover: both stores now write directly into the shared
|
||||||
},
|
// VtankProfiles directory, so the same collision risk exists there
|
||||||
],
|
// instead — resolved by the route store's "nav_" file-name prefix
|
||||||
});
|
// (metaf's own observed convention for a stand-alone nav .af).
|
||||||
|
[Fact]
|
||||||
|
public void MetaAndRouteProfilesWithTheSameNameDoNotCollide()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
storage.Text["imports/Same.met"] =
|
||||||
|
"1\r\nCondAct\r\n5\r\nCType\r\nAType\r\nCData\r\nAData\r\nState\r\n"
|
||||||
|
+ "n\r\nn\r\nn\r\nn\r\nn\r\n1\r\n"
|
||||||
|
+ "i\r\n1\r\ni\r\n2\r\ni\r\n0\r\ns\r\n/say imported\r\n"
|
||||||
|
+ "s\r\nDefault\r\n";
|
||||||
|
var panel = new MossTankPanel(new FakeHost(new FakeAutomation(), storage));
|
||||||
|
|
||||||
|
Command(panel, "meta load Same.met");
|
||||||
|
Command(panel, "meta save Same.met");
|
||||||
|
Command(panel, "nav save Same.nav");
|
||||||
|
|
||||||
|
Assert.True(storage.Text.ContainsKey("Same.af"));
|
||||||
|
Assert.True(storage.Text.ContainsKey("nav_Same.af"));
|
||||||
|
Assert.Contains(
|
||||||
|
"STATE: ",
|
||||||
|
storage.Text["Same.af"],
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"NAV: ",
|
||||||
|
storage.Text["nav_Same.af"],
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item J (slice-1 fix round): renamed from
|
||||||
|
// MetaCommandsImportAndExportExactVtankMetFiles for the same reason as
|
||||||
|
// the nav test above.
|
||||||
|
[Fact]
|
||||||
|
public void MetaCommandsImportLegacyAndExportAf()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
// Hand-authored CondAct binary payload (one rule: Always -> Chat
|
||||||
|
// "/say imported", state "Default") — VtankMetaProfileSerializer's
|
||||||
|
// writer was deleted (one-shot import only now), so this is built
|
||||||
|
// directly from the exact format its TryLoad still parses, matching
|
||||||
|
// condition type 1 (Always) / action type 2 (ChatCommand).
|
||||||
|
storage.Text["imports/Legacy.met"] =
|
||||||
|
"1\r\nCondAct\r\n5\r\nCType\r\nAType\r\nCData\r\nAData\r\nState\r\n"
|
||||||
|
+ "n\r\nn\r\nn\r\nn\r\nn\r\n1\r\n"
|
||||||
|
+ "i\r\n1\r\ni\r\n2\r\ni\r\n0\r\ns\r\n/say imported\r\n"
|
||||||
|
+ "s\r\nDefault\r\n";
|
||||||
var panel = new MossTankPanel(new FakeHost(
|
var panel = new MossTankPanel(new FakeHost(
|
||||||
new FakeAutomation(),
|
new FakeAutomation(),
|
||||||
storage));
|
storage));
|
||||||
|
|
@ -710,18 +1385,48 @@ public sealed class MossTankPanelTests
|
||||||
Assert.Single(panel.MetaRows);
|
Assert.Single(panel.MetaRows);
|
||||||
Assert.Contains("/say imported", panel.MetaRows[0], StringComparison.Ordinal);
|
Assert.Contains("/say imported", panel.MetaRows[0], StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// .af is the ONLY storage/authoring format now (Campaign VT slice 1
|
||||||
|
// Part A round 2 — the .met writer was deleted, one-shot import
|
||||||
|
// only, and the old "exports/meta/" side-car mirror is gone: this
|
||||||
|
// IS the real profile file).
|
||||||
Command(panel, "meta save Exported.met");
|
Command(panel, "meta save Exported.met");
|
||||||
Assert.StartsWith(
|
Assert.Contains(
|
||||||
"1\r\nCondAct\r\n5\r\n",
|
"STATE: ",
|
||||||
storage.Text["exports/Exported.met"],
|
storage.Text["Exported.af"],
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
Assert.True(VtankMetaProfileSerializer.TryLoad(
|
Assert.True(MetafSerializer.TryLoadMeta(
|
||||||
storage.Text["exports/Exported.met"],
|
storage.Text["Exported.af"],
|
||||||
|
NoOpSpellCatalogForExport.Instance,
|
||||||
out MetaProfile exported,
|
out MetaProfile exported,
|
||||||
out string error), error);
|
out string error), error);
|
||||||
Assert.Single(exported.Rules);
|
Assert.Single(exported.Rules);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Item J (slice-1 fix round): same doubled-extension bug as the nav
|
||||||
|
// side, "/vt meta save Foo.af".
|
||||||
|
[Fact]
|
||||||
|
public void MetaSaveAcceptsAnAfSuffixedNameWithoutDoublingIt()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
var panel = new MossTankPanel(new FakeHost(new FakeAutomation(), storage));
|
||||||
|
|
||||||
|
Command(panel, "meta save Foo.af");
|
||||||
|
|
||||||
|
Assert.True(storage.Text.ContainsKey("Foo.af"));
|
||||||
|
Assert.False(storage.Text.ContainsKey("Foo.af.af"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NoOpSpellCatalogForExport : ISpellCatalog
|
||||||
|
{
|
||||||
|
public static NoOpSpellCatalogForExport Instance { get; } = new();
|
||||||
|
public IReadOnlyList<PluginSpellInfo> KnownSelfBuffs => [];
|
||||||
|
public bool TryGet(uint spellId, out PluginSpellInfo info)
|
||||||
|
{
|
||||||
|
info = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ByCharacterProfilesAreIsolatedByCharacterName()
|
public void ByCharacterProfilesAreIsolatedByCharacterName()
|
||||||
{
|
{
|
||||||
|
|
@ -1014,6 +1719,97 @@ public sealed class MossTankPanelTests
|
||||||
"uboptget['AttackDistance']").AsNumber(), precision: 7);
|
"uboptget['AttackDistance']").AsNumber(), precision: 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round 3 item 6: retail's bk.a text (refs/vtank/decompiled/bk.cs:32)
|
||||||
|
/// is "Done saving setting X to all profiles. (Changed N profiles)" —
|
||||||
|
/// no "= value" suffix, unlike the plain "set" success message.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void VtankSetInAllReportsRetailsExactMessageText()
|
||||||
|
{
|
||||||
|
var automation = new FakeAutomation();
|
||||||
|
var panel = new MossTankPanel(new FakeHost(automation));
|
||||||
|
|
||||||
|
Command(panel, "opt setinall AttackDistance 0.03");
|
||||||
|
|
||||||
|
string message = Assert.Single(automation.Messages, static text =>
|
||||||
|
text.StartsWith("Done saving setting", StringComparison.Ordinal));
|
||||||
|
Assert.StartsWith(
|
||||||
|
"Done saving setting AttackDistance to all profiles. (Changed ",
|
||||||
|
message,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.EndsWith(" profiles)", message, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("=", message, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round 3 item 6: a value that does not parse as the catalog's own
|
||||||
|
/// declared type (EnableLooting is Bool) fails with retail's exact
|
||||||
|
/// text (refs/vtank/decompiled/uTank2/PluginCore.cs:5501,5508,5612).
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("set")]
|
||||||
|
[InlineData("setinall")]
|
||||||
|
public void VtankOptSetRejectsAWrongTypedValueWithRetailsExactText(string operation)
|
||||||
|
{
|
||||||
|
var automation = new FakeAutomation();
|
||||||
|
var panel = new MossTankPanel(new FakeHost(automation));
|
||||||
|
|
||||||
|
Command(panel, $"opt {operation} EnableLooting notaboolean");
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
"Option set: Invalid value specified. Proper type of EnableLooting is System.Boolean.",
|
||||||
|
Assert.Single(automation.Messages));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round 3 item 6: <c>bk.a</c> (refs/vtank/decompiled/bk.cs:22-27)
|
||||||
|
/// APPENDS a new Settings row — [0]=name, [1]=value, every other column
|
||||||
|
/// left as a void <c>gy()</c> cell — when a <c>.usd</c> file's Settings
|
||||||
|
/// table has no row for the option at all, rather than skipping that
|
||||||
|
/// file outright.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void SetOptionInAllAppendsARowWhenAFileHasNoneForThatSetting()
|
||||||
|
{
|
||||||
|
var storage = new MemoryStorage();
|
||||||
|
var automation = new FakeAutomation { Name = "Barris" };
|
||||||
|
// A hand-trimmed Settings table (4 real columns: Setting, Value,
|
||||||
|
// Description, SettingType) with exactly one row, for a DIFFERENT
|
||||||
|
// setting — "EnableLooting" is deliberately absent.
|
||||||
|
const string minimalUsd =
|
||||||
|
"1\r\nSettings\r\n4\r\nSetting\r\nValue\r\nDescription\r\nSettingType\r\n"
|
||||||
|
+ "y\r\nn\r\nn\r\nn\r\n1\r\ns\r\nEnableNav\r\nb\r\nFalse\r\ns\r\n\r\ni\r\n1\r\n";
|
||||||
|
storage.Text["Other.usd"] = minimalUsd;
|
||||||
|
|
||||||
|
var store = new MossTankProfileStore(new FakeHost(automation, storage));
|
||||||
|
var settings = new VtankSettingsProfileSerializer.AllSettings
|
||||||
|
{
|
||||||
|
Combat = new CombatSettings(),
|
||||||
|
Buffs = new BuffSettings(),
|
||||||
|
Vitals = new VitalSettings(),
|
||||||
|
Inventory = new InventorySettings(),
|
||||||
|
Navigation = new NavigationSettings(),
|
||||||
|
};
|
||||||
|
settings.Inventory.Loot.Enabled = true;
|
||||||
|
|
||||||
|
int count = store.SetOptionInAll("EnableLooting", settings);
|
||||||
|
|
||||||
|
VtankDatabase rewritten = VtankDatabase.Parse(storage.Text["Other.usd"]);
|
||||||
|
VtankTable table = rewritten.Find("Settings")!;
|
||||||
|
int nameColumn = table.ColumnIndex("Setting");
|
||||||
|
int valueColumn = table.ColumnIndex("Value");
|
||||||
|
VtankRow appended = Assert.Single(table.Rows, row =>
|
||||||
|
row.Cells[nameColumn].AsString().Equals(
|
||||||
|
"EnableLooting", StringComparison.OrdinalIgnoreCase));
|
||||||
|
Assert.True(appended.Cells[valueColumn].AsBool());
|
||||||
|
// The remaining columns (Description/SettingType) stay void, never
|
||||||
|
// copied from the existing EnableNav row.
|
||||||
|
Assert.Equal("0", appended.Cells[2].Tag);
|
||||||
|
Assert.Equal("0", appended.Cells[3].Tag);
|
||||||
|
Assert.True(count >= 1);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void VtankHelpAndExpressionsUseTheLocalCommandSurface()
|
public void VtankHelpAndExpressionsUseTheLocalCommandSurface()
|
||||||
{
|
{
|
||||||
|
|
@ -1387,10 +2183,14 @@ public sealed class MossTankPanelTests
|
||||||
private sealed class FakeHost(
|
private sealed class FakeHost(
|
||||||
IAutomationSurface automation,
|
IAutomationSurface automation,
|
||||||
IPluginStorage? storage = null,
|
IPluginStorage? storage = null,
|
||||||
IPluginLootClassifierRegistry? lootClassifiers = null) : IPluginHost
|
IPluginLootClassifierRegistry? lootClassifiers = null,
|
||||||
|
IPluginStorage? vtankProfiles = null) : IPluginHost
|
||||||
{
|
{
|
||||||
public bool HasUi => false;
|
public bool HasUi => false;
|
||||||
public IPluginLogger Log { get; } = new FakeLogger();
|
// Round 3 item 12: exposed as the concrete FakeLogger (not just the
|
||||||
|
// IPluginHost.Log interface view) so a test can inspect Warnings.
|
||||||
|
public FakeLogger Logger { get; } = new();
|
||||||
|
public IPluginLogger Log => Logger;
|
||||||
public IGameState State { get; } = new FakeState();
|
public IGameState State { get; } = new FakeState();
|
||||||
public IEvents Events { get; } = new FakeEvents();
|
public IEvents Events { get; } = new FakeEvents();
|
||||||
public ISelectionService Selection { get; } = new FakeSelection();
|
public ISelectionService Selection { get; } = new FakeSelection();
|
||||||
|
|
@ -1400,6 +2200,14 @@ public sealed class MossTankPanelTests
|
||||||
public IAutomationSurface Automation { get; } = automation;
|
public IAutomationSurface Automation { get; } = automation;
|
||||||
public IPluginLootClassifierRegistry LootClassifiers { get; } =
|
public IPluginLootClassifierRegistry LootClassifiers { get; } =
|
||||||
lootClassifiers ?? NoOpPluginLootClassifierRegistry.Instance;
|
lootClassifiers ?? NoOpPluginLootClassifierRegistry.Instance;
|
||||||
|
// Real VTank .usd/.af/.cdf storage. Defaults to the SAME backing
|
||||||
|
// store as the plugin's own JSON storage when a test supplies one
|
||||||
|
// (the key namespaces never collide: JSON keys are always "/"-
|
||||||
|
// prefixed, VTank file names never are) so existing tests keep
|
||||||
|
// exercising real persistence without every call site needing a
|
||||||
|
// second storage instance.
|
||||||
|
public IPluginStorage VtankProfiles { get; } =
|
||||||
|
vtankProfiles ?? storage ?? NoOpPluginStorage.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class FakeLootClassifierRegistry(
|
private sealed class FakeLootClassifierRegistry(
|
||||||
|
|
@ -1706,8 +2514,13 @@ public sealed class MossTankPanelTests
|
||||||
|
|
||||||
private sealed class FakeLogger : IPluginLogger
|
private sealed class FakeLogger : IPluginLogger
|
||||||
{
|
{
|
||||||
|
// Round 3 item 12: capture Warn() calls so a test can prove a
|
||||||
|
// corrupt monster-rule expression is logged rather than silently
|
||||||
|
// swallowed by the two former bare "catch (FormatException) { }"
|
||||||
|
// blocks in MossTankProfileStore.
|
||||||
|
public List<string> Warnings { get; } = [];
|
||||||
public void Info(string message) { }
|
public void Info(string message) { }
|
||||||
public void Warn(string message) { }
|
public void Warn(string message) => Warnings.Add(message);
|
||||||
public void Error(string message, Exception? exception = null) { }
|
public void Error(string message, Exception? exception = null) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1719,7 +2532,8 @@ public sealed class MossTankPanelTests
|
||||||
public string? ReadText(string key) =>
|
public string? ReadText(string key) =>
|
||||||
Text.TryGetValue(key, out string? value) ? value : null;
|
Text.TryGetValue(key, out string? value) ? value : null;
|
||||||
public IReadOnlyList<string> List(string prefix) => Text.Keys
|
public IReadOnlyList<string> List(string prefix) => Text.Keys
|
||||||
.Where(key => key.StartsWith(prefix + "/", StringComparison.Ordinal))
|
.Where(key => prefix.Length == 0
|
||||||
|
|| key.StartsWith(prefix + "/", StringComparison.Ordinal))
|
||||||
.OrderBy(static key => key, StringComparer.Ordinal)
|
.OrderBy(static key => key, StringComparer.Ordinal)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
public void WriteText(string key, string content) => Text[key] = content;
|
public void WriteText(string key, string content) => Text[key] = content;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace AcDream.Plugins.MossTank.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round 3 item 1: proves <c>MossTankProfileStore.SideCarDocument</c> (the
|
||||||
|
/// private JSON side-car for state that has no VTank Settings-table name at
|
||||||
|
/// all) carries no field that ALSO has a real <c>.usd</c> catalog name —
|
||||||
|
/// any such overlap is a live duplication risk exactly like the
|
||||||
|
/// <c>InventoryEnableLooting</c>/<c>EnableLooting</c> bug this round fixed
|
||||||
|
/// (the side-car's own copy is always applied AFTER the <c>.usd</c> row, so
|
||||||
|
/// a stale or default side-car value silently clobbers whatever the real
|
||||||
|
/// profile file says).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MossTankProfileStoreSideCarTests
|
||||||
|
{
|
||||||
|
// The side-car's own property names are grouped by settings section
|
||||||
|
// (Combat*/Buff*/Vitals*/Inventory*) purely for readability — VTank's
|
||||||
|
// catalog names carry no such prefix, so each candidate is checked both
|
||||||
|
// as typed and with its known group prefix stripped.
|
||||||
|
private static readonly string[] GroupPrefixes = ["Combat", "Buff", "Vitals", "Inventory"];
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SideCarDocumentHasNoFieldNamedForARealVtankSetting()
|
||||||
|
{
|
||||||
|
Type sideCar = typeof(MossTankProfileStore).GetNestedType(
|
||||||
|
"SideCarDocument", BindingFlags.NonPublic)
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"MossTankProfileStore.SideCarDocument was not found by reflection.");
|
||||||
|
var catalogNames = new HashSet<string>(
|
||||||
|
VtankOptionCatalog.Names, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var overlaps = new List<string>();
|
||||||
|
foreach (PropertyInfo property in sideCar.GetProperties(
|
||||||
|
BindingFlags.Public | BindingFlags.Instance))
|
||||||
|
{
|
||||||
|
string name = property.Name;
|
||||||
|
if (catalogNames.Contains(name))
|
||||||
|
{
|
||||||
|
overlaps.Add(name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach (string prefix in GroupPrefixes)
|
||||||
|
{
|
||||||
|
if (name.Length > prefix.Length
|
||||||
|
&& name.StartsWith(prefix, StringComparison.Ordinal)
|
||||||
|
&& catalogNames.Contains(name[prefix.Length..]))
|
||||||
|
{
|
||||||
|
overlaps.Add(name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Empty(overlaps);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue