Skip to content
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.
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 @@ -94,6 +94,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 @@ -94,6 +94,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 @@ -94,6 +94,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 @@ -94,6 +94,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
10 changes: 10 additions & 0 deletions example_community_patch_settings_en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,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 Expand Up @@ -263,6 +270,9 @@ loadingtiphooks = true
doubleclickassignshiphooks = true
forbiddentechconfirmhooks = true
audioeventhooks = true
# Install queue hooks independently of the current control preferences.
actionqueuerecoveryhooks = true
thinqueueprotectionhooks = true
freeresizehooks = true
game_version = true
giftsbulkclaimhooks = 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 @@ -94,6 +94,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 @@ -94,6 +94,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 @@ -94,6 +94,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 @@ -94,6 +94,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 @@ -94,6 +94,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
9 changes: 9 additions & 0 deletions mods/src/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,10 @@ 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->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->installAudioEventHooks =
get_config_or_default(config, parsed, "patches", "audioeventhooks", DCP::audioeventhooks, write_config);
this->installInstantCargoCounterHooks =
Expand All @@ -934,6 +938,11 @@ void Config::Load()
this->installPinnedShipSortHooks =
get_config_or_default(config, parsed, "patches", "pinnedshiphooks", DCP::pinnedshiphooks, write_config);
spdlog::debug("");
this->faster_queue_recovery = get_config_or_default(config, parsed, "control", "faster_queue_recovery",
DCC::faster_queue_recovery, 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
4 changes: 4 additions & 0 deletions mods/src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ class Config final
int select_timer;

bool queue_enabled;
bool faster_queue_recovery;
bool thin_queue_protection;
bool hotkeys_enabled;
bool hotkeys_extended;
bool use_scopely_hotkeys;
Expand Down Expand Up @@ -278,6 +280,8 @@ class Config final
bool installForbiddenTechConfirmationHooks;
bool installInstantWarpConfirmationHooks;
bool installAudioEventHooks;
bool installActionQueueRecoveryHooks;
bool installThinQueueProtectionHooks;

std::string config_settings_url;
std::string config_assets_url_override;
Expand Down
4 changes: 4 additions & 0 deletions mods/src/defaultconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ namespace Control
constexpr bool hotkeys_extended = true;
constexpr bool use_scopely_hotkeys = false;
constexpr bool queue_enabled = true;
constexpr bool faster_queue_recovery = false;
constexpr bool thin_queue_protection = true;
constexpr auto select_timer = 500;
} // namespace Control

Expand Down Expand Up @@ -94,6 +96,8 @@ namespace Patches
constexpr bool doubleclickassignshiphooks = true;
constexpr bool forbiddentechconfirmhooks = true;
constexpr bool audioeventhooks = true;
constexpr bool actionqueuerecoveryhooks = true;
constexpr bool thinqueueprotectionhooks = true;
constexpr bool instantcargocounterhooks = true;
constexpr bool cargoformathooks = true; // on by default: cargo number precision override
constexpr bool officersorthooks = true; // restore Below Deck Ability sort option
Expand Down
44 changes: 44 additions & 0 deletions mods/src/il2cpp/method_contract.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#pragma once

#include "il2cpp-functions.h"
#include <il2cpp-tabledefs.h>
#include <cstring>
#include <initializer_list>

namespace method_contract
{
inline bool Type(const Il2CppType* type, const char* name)
{
if (!type || type->byref) return false;
auto* actual = il2cpp_type_get_name(type);
const bool matches = actual && std::strcmp(actual, name) == 0;
il2cpp_free(actual);
return matches;
}

// Resolve the entire managed signature, including static/instance dispatch.
// A renamed, ambiguous or generic method is not a compatible callback.
inline const MethodInfo* Resolve(Il2CppClass* cls, const char* name, bool is_static,
const char* result, std::initializer_list<const char*> parameters)
{
if (!cls) return nullptr;
const MethodInfo* found = nullptr;
void* iterator = nullptr;
while (auto* method = il2cpp_class_get_methods(cls, &iterator)) {
if (std::strcmp(method->name, name) != 0 || !method->methodPointer || method->is_generic || method->is_inflated
|| bool(method->flags & METHOD_ATTRIBUTE_STATIC) != is_static
|| method->parameters_count != parameters.size() || !Type(method->return_type, result)) continue;
bool matches = true;
unsigned i = 0;
for (auto* parameter : parameters)
matches = Type(method->parameters[i++], parameter) && matches;
if (!matches) continue;
if (found) return nullptr;
found = method;
}
return found;
}

inline void* Pointer(const MethodInfo* method)
{ return method ? reinterpret_cast<void*>(method->methodPointer) : nullptr; }
} // namespace method_contract
Loading
Loading