Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs/FASTER_QUEUE_RECOVERY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Faster Queue Recovery

When a queued target disappears while a course request is outstanding, the game can remove that target before its failed response arrives. Native failure handling then attempts the removal again. Since it did not remove the front entry this time, it can leave the next target waiting for the watchdog.

Enable the opt-in recovery preference:

```toml
[control]
faster_queue_recovery = true
```

Following #313, default-enabled `[patches].actionqueuerecoveryhooks` independently controls installation. Changing that installation switch requires a restart. Installed callbacks check the current `control.faster_queue_recovery` and `control.queue_enabled` preferences; changing TOML still requires reload/restart.

The feature also respects `control.queue_enabled`. Hooks are supported on Windows x64 when the required method signatures and queue layout are compatible. Other platforms do not install these hooks; incompatible Windows layouts log an unavailable message and retain native behavior.

## Behavior

The adapter records the latest engagement attempt for up to eight fleets, using weak queue identities and full 64-bit target IDs. A failed, non-recall course response may request native planning only when:

- The response belongs to the same queue and latest target/attempt, observed within 30 seconds.
- The queue was engaging on entry to the response and native processing cleared that flag.
- A different front target remains, and the failed target is absent from every inspected queue.
- The native retry decision was false. Ordinary retries are unchanged.

The request record is consumed once. The native planner selects and validates the next target. The mod does not force ship state, clear engagement flags, change retry counts, or remove targets. If another fleet still queues the target, the native cross-fleet removal path remains responsible.

Queue storage is bounded and validated. Unknown layouts/content are ineligible. Expired/replaced requests release their weak handles; native session cleanup clears all records. Engage, Retry and Course discard pending recovery records when they observe disabled behavior. Both Toggle Queue transitions also clear them, so an off/on toggle cannot reuse authorization from before the toggle. A native attempt returning skip/stop cancels only its own record, preserving newer reentrant attempts.

There is no watchdog hook, frame scan, timer, background worker, or per-engagement logging. Queue inspection occurs in existing callbacks; all-queue inspection runs only for a potentially eligible failed response. Response matching is not a server-issued request ID, so delayed same-target responses remain an interoperability limitation.

## Native integration

One detour owns each method. Methods are resolved through their complete managed signatures. Installation validates the event and queue field layouts and the engagement result's integer representation. SPUD builds the trampolines using its existing instruction decoder. Addresses and relocation-dependent instruction bytes are not pinned to a client release.

The hooked methods are `TryPlanPathAndEngageTarget`, `ShouldRetryFailedSetCourse`, `OnSetCourseResponseEventHandler` and `StopWatchdogAndClearAllQueues`. These checks establish binding compatibility; changes to the native retry/planner behavior still require update review and runtime testing.

The course event is a 24-byte value type, with fleet ID at 0, success/recall at 8/9 and boxed target at 16. Metadata field offsets include the boxed object header. The native retry handler is called synchronously inside the course handler; returning true selects its existing planner branch.

The older off-screen Kir'Shara combat-completion repair was fixed by Scopely. That workaround and its `kirshara_queue_repair` setting have already been removed; neither is a prerequisite for Faster Queue Recovery. This feature addresses only the separate unavailable-target/course-response race described above.

## Validation

Run `tests/run-action-queue.ps1` on Windows, or compile `tests/action_queue.cc` with a C++23 compiler and `-Imods/src`. Tests use the production policy and request store with fake weak handles. They cover one-shot recovery, reordered/still-present targets, cross-fleet rejection, stale attempts, replaced/collected queues, expiry, reentrant cancellation, bounded capacity and cleanup. They do not model game ABI or network scheduling.

The prototype produced two observed handoffs with the next target attempted 1–2 ms later and successful responses within 388–532 ms. Those timings are observations, not a latency guarantee. The final adapter requires its own smoke test: normal queued combat; removal of the outstanding target; removal before the first successful course response; queue clear/rebuild; recall; and session restart. Group-wave behavior needs additional coverage when available.
38 changes: 38 additions & 0 deletions docs/QUEUE_ADDRESS_GUARD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Kir'Shara queue address guard

Moving or recalling another ship in a different system can temporarily make a
deployed ship's native player address point at its starbase. A subsequent native
player-state event then treats that ship's same-system queued targets as foreign
and clears its queue. This failure was also reproduced by a tester with the mod
DLL disabled.

The default-on guard substitutes the ship's current owned deployment address only
inside the native queue address comparison during a player-state event. It requires
a validated nonempty queue belonging to that ship and slot, a local deployment
whose fleet ID matches, no recall or removal, and an active non-warping ship state.
The reported address must be a planet in another system, and the current target
must match the deployment's full galaxy/system/planet/instance address.

The native comparison still checks every target. A missing target, a genuinely
foreign address, or unknown metadata retains native behavior. The guard writes no
fleet fields or queue contents and retains no address between events. Explicit
clearing, recalling the queued ship itself, and genuine system changes remain
native. This is independent of Faster Queue Recovery and Thin Queue Protection.

`[control] queue_address_guard = false` disables correction. The separate
default-on `[patches] queueaddressguardhooks` switch controls hook installation;
changing it requires a restart. Disabling the queue also disables correction.

The implementation is shared by Windows and macOS and contains no capture logger,
polling loop, or diagnostic hooks. It owns four native methods: the player-state
event handler, the queue address predicate, the player address getter, and the
deployment lookup. Their Windows x64 client 271 native overwrite windows were
measured against the matching installed client. Current Mac native hook fit and
gameplay validation have not been established; successful builds alone do not
establish those properties.

Run `tests/run-action-queue.ps1` for local regression checks. Gameplay validation
must use the clean build: queue A's same-system hostiles, move/recall B in another
system, and allow A to engage its next target. Then recall A and check its queue
clears normally. The prior instrumented build passed both controls, and reproduced
the raw address fault even with fleet observation polling disabled.
50 changes: 50 additions & 0 deletions docs/THIN_QUEUE_PROTECTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Thin Queue Protection

Restores the active guard from `v2.1.0-guffa.10` alongside Faster Queue Recovery.
This is separate from the retired Kir'shara combat-completion repair.

```toml
[control]
thin_queue_protection = true
```

Enabled by default on Windows x64. Also respects `control.queue_enabled`. The old
`advanced.queue.thin_queue_protection` value is used when the new control key is
absent, preserving an explicit opt-out. Default-enabled `[patches].thinqueueprotectionhooks`
controls installation independently. Installed callbacks check the current feature and queue
preferences. Changing the installation switch requires restart; TOML changes require
reload/restart.

The native planner (`DoPlanPathAndEngageTarget`) and watchdog (`HandleStall`) run
first. If they removed an exact prefix of targets, left a nonempty unchanged
suffix, and left the same fleet idle without an active engagement, the guard
rechecks that state and asks the native `TryPlanPathAndEngageTarget` to resume.
Pending/last-target latches must be absent or refer only to removed targets.
Reordering, replacement, truncation, or a latch naming a surviving target rejects
recovery. An unchanged queue is never enough evidence to retry.

On disposal, a fleet must be explicitly destroyed with removal reason `Destroyed`
(1). If native handling leaves that target at an inactive queue head, the guard
rechecks it and calls native `ProcessQueue(target, false)`. It does not force
immediate selection, remove arbitrary targets, or edit queue/engagement state.

Faster Queue Recovery still owns the failed-course-response path. The restored
guard uses three different detours and calls the existing native engage entry,
so Faster Queue Recovery can observe those requests normally. The nested planner
and watchdog paths cannot replay a successfully engaged target because the outer
guard sees the resulting active engagement. Queue identity is checked again after
native calls. All three guard hooks must install before any guard action is enabled.

Methods, field offsets/types and enum return representations are checked before
installation; incompatible layouts keep native behavior. This port currently
installs only on Windows x64. macOS runtime/ABI validation is not claimed.

Successful actions log `[ThinQueueProtection] resume` or
`[ThinQueueProtection] process-destroyed-head`; startup logs `ready=true` or an
unavailable warning. There is no per-frame scan or background polling.

Run `tests/run-action-queue.ps1` for both recovery-policy suites. The restored
release tests cover destroyed-head filtering, prefix removal, multiple removed
targets, latch safety, queue replacement/reordering and postcondition changes.
They do not establish game ABI or live wave behavior. Runtime verification still
requires a new build to be deployed and a wave test after restart.
6 changes: 6 additions & 0 deletions example_community_patch_settings_da.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ hotkeys_extended = true
# Skal køen være aktiveret som standard, hvis du har Kir'Shara-artefakten?
queue_enabled = true

# Hurtigere køgendannelse, når et ventende mål forsvinder (kun Windows x64).
faster_queue_recovery = false

# Beskytter køen efter oprydning af mål (kun Windows x64; aktiveret som standard).
thin_queue_protection = true

# Sæt denne til sand, hvis du foretrækker at bruge Scopelys genvejstaster
use_scopely_hotkeys = false

Expand Down
7 changes: 7 additions & 0 deletions example_community_patch_settings_de.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ hotkeys_extended = true
# Soll die Warteschlange standardmäßig aktiviert sein, wenn das Kir'Shara-Artefakt vorhanden ist?
queue_enabled = true

# Schnellere Warteschlangen-Wiederaufnahme, wenn ein Ziel verschwindet (nur Windows x64).
faster_queue_recovery = false

# Thin Queue Protection: nach nativer Bereinigung fortsetzen; zerstörte Ziele am Anfang entfernen.
# Nur Windows x64. Standardmäßig aktiv; unabhängig von Faster Queue Recovery.
thin_queue_protection = true

# Auf true setzen, um die Hotkeys von Scopely zu verwenden
use_scopely_hotkeys = false

Expand Down
7 changes: 7 additions & 0 deletions example_community_patch_settings_en-GB-x-cockney.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ hotkeys_extended = true
# If you have the Kir'Shara artifact, should the queue be enabled by default?
queue_enabled = true

# Faster Queue Recovery: advance after an outstanding target disappears (Windows x64 only).
faster_queue_recovery = false

# Thin Queue Protection: resume after native pruning; clean up confirmed destroyed heads.
# Windows x64 only. Enabled by default; independent of Faster Queue Recovery.
thin_queue_protection = true

# If you prefer to use Scopely's hotkeys set this to true
use_scopely_hotkeys = false

Expand Down
7 changes: 7 additions & 0 deletions example_community_patch_settings_en-x-minionese.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ hotkeys_extended = true
# If you have the Kir'Shara artifact, should the queue be enabled by default?
queue_enabled = true

# Faster Queue Recovery: advance after an outstanding target disappears (Windows x64 only).
faster_queue_recovery = false

# Thin Queue Protection: resume after native pruning; clean up confirmed destroyed heads.
# Windows x64 only. Enabled by default; independent of Faster Queue Recovery.
thin_queue_protection = true

# If you prefer to use Scopely's hotkeys set this to true
use_scopely_hotkeys = false

Expand Down
15 changes: 15 additions & 0 deletions example_community_patch_settings_en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,17 @@ hotkeys_extended = true
# If you have the Kir'Shara artifact, should the queue be enabled by default?
queue_enabled = true

# Faster Queue Recovery: advance after an outstanding target disappears (Windows x64 only).
faster_queue_recovery = false

# Thin Queue Protection: resume after native pruning; clean up confirmed destroyed heads.
# Windows x64 only. Enabled by default; independent of Faster Queue Recovery.
thin_queue_protection = true

# Keep a deployed ship's queue when another ship causes a temporary starbase address.
# Independent of recovery settings; legitimate recall/warp/manual clearing stays native.
queue_address_guard = true

# If you prefer to use Scopely's hotkeys set this to true
use_scopely_hotkeys = false

Expand Down Expand Up @@ -277,6 +288,10 @@ chatpatches = true
dailyfactionbulkclaimhooks = true
doubleclickassignshiphooks = true
forbiddentechconfirmhooks = true
# Install queue hooks independently of the current control preferences.
actionqueuerecoveryhooks = true
queueaddressguardhooks = true
thinqueueprotectionhooks = true
freeresizehooks = true
galacticanomalytimerhooks = true
game_version = true
Expand Down
6 changes: 6 additions & 0 deletions example_community_patch_settings_es.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ hotkeys_extended = true
# Si tienes el artefacto Kir'Shara, ¿debe activarse la cola de forma predeterminada?
queue_enabled = true

# Recuperación rápida de la cola cuando desaparece un objetivo pendiente (solo Windows x64).
faster_queue_recovery = false

# Protege la cola tras eliminar objetivos (solo Windows x64; activado por defecto).
thin_queue_protection = true

# Si prefieres usar los atajos de Scopely, establece este valor en verdadero
use_scopely_hotkeys = false

Expand Down
7 changes: 7 additions & 0 deletions example_community_patch_settings_fr.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ hotkeys_extended = true
# Si vous avez l’artefact Kir’Shara, la file d’attente doit-elle être activée par défaut ?
queue_enabled = true

# Reprise plus rapide de la file si une cible disparaît (Windows x64 uniquement).
faster_queue_recovery = false

# Thin Queue Protection : reprendre après le nettoyage natif des cibles en tête de file.
# Windows x64 uniquement. Activé par défaut ; indépendant de Faster Queue Recovery.
thin_queue_protection = true

# Si vous préférez utiliser les raccourcis de Scopely mettez ça sur activer
use_scopely_hotkeys = false

Expand Down
7 changes: 7 additions & 0 deletions example_community_patch_settings_nl.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ hotkeys_extended = true
# Als je de Kir'Shara artefect hebt, moet de wachtrij standaard ingeschakeld zijn?
queue_enabled = true

# Sneller doorgaan met de wachtrij als een doel verdwijnt (alleen Windows x64).
faster_queue_recovery = false

# Thin Queue Protection: hervat na native opruiming; verwijder bevestigde vernietigde doelen vooraan.
# Alleen Windows x64. Standaard aan; onafhankelijk van Faster Queue Recovery.
thin_queue_protection = true

# Als je de Scopely sneltoetsen prefereerd over die van de mod, schakel dit dan aan
use_scopely_hotkeys = false

Expand Down
6 changes: 6 additions & 0 deletions example_community_patch_settings_ru.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ hotkeys_extended = true
# Следует ли включать очередь по умолчанию при наличии артефакта Кир'Шара?
queue_enabled = true

# Быстрое восстановление очереди, если ожидаемая цель исчезла (только Windows x64).
faster_queue_recovery = false

# Защита очереди после удаления целей (только Windows x64; включена по умолчанию).
thin_queue_protection = true

# Включите этот параметр, если предпочитаете горячие клавиши Scopely
use_scopely_hotkeys = false

Expand Down
7 changes: 7 additions & 0 deletions example_community_patch_settings_tlh.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ hotkeys_extended = true
# If you have Kir'Shara artifact, should queue be chu' by motlh?
queue_enabled = true

# Faster Queue Recovery: advance after an outstanding target disappears (Windows x64 only).
faster_queue_recovery = false

# Thin Queue Protection: resume after native pruning; clean up confirmed destroyed heads.
# Windows x64 only. Enabled by default; independent of Faster Queue Recovery.
thin_queue_protection = true

# If you prefer Daq lo' Scopely's hotkeys cher this Daq teH
use_scopely_hotkeys = false

Expand Down
15 changes: 14 additions & 1 deletion mods/src/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ constexpr bool is_all_audio_wildcard(std::string_view value)
static_assert(is_all_audio_wildcard("aLl"));
static_assert(!is_all_audio_wildcard(std::string_view{"All\0suffix", 10}));
static_assert(!is_all_audio_wildcard(std::string_view{"\xffll", 3}));

struct ToastAudioAlertConfig {
int toast_state;
std::string_view config_name;
Expand Down Expand Up @@ -995,6 +995,12 @@ void Config::Load()
get_config_or_default(config, parsed, "patches", "doubleclickassignshiphooks", DCP::doubleclickassignshiphooks, write_config);
this->installForbiddenTechConfirmationHooks =
get_config_or_default(config, parsed, "patches", "forbiddentechconfirmhooks", DCP::forbiddentechconfirmhooks, write_config);
this->installQueueAddressGuardHooks = get_config_or_default(
config, parsed, "patches", "queueaddressguardhooks", DCP::queueaddressguardhooks, write_config);
this->installActionQueueRecoveryHooks =
get_config_or_default(config, parsed, "patches", "actionqueuerecoveryhooks", DCP::actionqueuerecoveryhooks, write_config);
this->installThinQueueProtectionHooks =
get_config_or_default(config, parsed, "patches", "thinqueueprotectionhooks", DCP::thinqueueprotectionhooks, write_config);
this->installArtifactExchangeHooks =
get_config_or_default(config, parsed, "patches", "artifactexchangehooks", DCP::artifactexchangehooks, write_config);
this->installAudioEventHooks =
Expand All @@ -1010,6 +1016,13 @@ void Config::Load()
this->installHavenHistoryHooks =
get_config_or_default(config, parsed, "patches", "havenhistoryhooks", DCP::havenhistoryhooks, write_config);
spdlog::debug("");
this->faster_queue_recovery = get_config_or_default(config, parsed, "control", "faster_queue_recovery",
DCC::faster_queue_recovery, write_config);
this->queue_address_guard = get_config_or_default(
config, parsed, "control", "queue_address_guard", DCC::queue_address_guard, write_config);
// Preserve an explicit 2.1.0 setting; the current control key takes precedence.
this->thin_queue_protection = get_config_or_default(config, parsed, "control", "thin_queue_protection",
config["advanced"]["queue"]["thin_queue_protection"].value_or(DCC::thin_queue_protection), write_config);
this->queue_enabled =
get_config_or_default(config, parsed, "control", "queue_enabled", DCC::queue_enabled, write_config);
this->hotkeys_enabled =
Expand Down
6 changes: 6 additions & 0 deletions mods/src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ class Config final
int select_timer;

bool queue_enabled;
bool faster_queue_recovery;
bool thin_queue_protection;
bool queue_address_guard;
Comment on lines +192 to +194

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a purpose to having three hook methods? Would not all these be desired to be enabled together?

bool hotkeys_enabled;
bool hotkeys_extended;
bool use_scopely_hotkeys;
Expand Down Expand Up @@ -297,6 +300,9 @@ class Config final
bool installForbiddenTechConfirmationHooks;
bool installInstantWarpConfirmationHooks;
bool installAudioEventHooks;
bool installActionQueueRecoveryHooks;
bool installQueueAddressGuardHooks;
bool installThinQueueProtectionHooks;
bool installArtifactExchangeHooks;

std::string config_settings_url;
Expand Down
Loading
Loading