Persistent DockAndWait Order Cancellation Issue

The place to discuss scripting and game modifications for X4: Foundations.

Moderators: Scripting / Modding Moderators, Moderators for English X Forum

staeuber
Posts: 33
Joined: Tue, 8. Feb 05, 00:36
x4

Persistent DockAndWait Order Cancellation Issue

Post by staeuber »

X4 Modding Help Request: DockAndWait Order Cancelled After Successful Docking

Dear community, I hope someone can help me with this issue!

I edited my last post to be more precise by providing an easy example that anyone can test.

Problem Summary

I'm experiencing a persistent issue with custom AI scripts where ships dock successfully but immediately undock due to premature DockAndWait order cancellation. This creates an infinite dock/undock loop that I can't resolve despite following vanilla X4 patterns exactly.

🔬 COMPLETE TEST CASE ATTACHED

I've created a minimal test mod that reproduces this issue 100% of the time. The complete test case is attached as a ZIP file - you can drop it in your X4 extensions folder and see the exact same behavior.

Test Mod Contents:
order.test.dosomething.xml - Master order that picks random stations
order.test.trainperform.xml - Training order that exhibits the dock/undock issue
Comprehensive logging patches - Shows exactly what's happening

🚫 The Issue

What should happen:
1. Ship creates DockAndWait order
2. Ship docks successfully
3. TrainPerform script continues with training
4. Ship stays docked during training

What actually happens:
1. Ship creates DockAndWait order ✅
2. Ship docks successfully ✅
3. DockAndWait confirms "waiting" state ✅
4. Ship undocks immediately ❌
5. Script takes ~8 seconds to restart and detect undocking ❌
6. TrainPerform creates new DockAndWait order ❌
7. Infinite loop ❌

📋 Latest Debug Logs

Code: Select all

[Scripts] 290.40: [TEST-TRAIN] ❌ Not docked. Creating DockAndWait and waiting for completion
[Scripts] 380.23: [TEST-PATCH] DockAndWait: === DOCK SUCCESS === Now waiting for caller to complete operations...
[Scripts] 380.23: Kurier Verteidiger successfully docked at ARG Mikrochipfabrik I. waiting.

!! SHIP UNDOCKS MYSTERIOUSLY !!

[Scripts] 388.44: [TEST-TRAIN] === TRAINING START === Ship: KPC-834, Destination: ARG Mikrochipfabrik I  
[Scripts] 388.44: [TEST-TRAIN] ❌ Not docked. Creating DockAndWait...
The DockAndWait order succeeds and confirms it's waiting, but the ship undocks immediately. The script then takes ~8 seconds to restart and detect the ship is no longer docked, creating a new DockAndWait order and repeating the cycle.

🔍 Current Implementation

TrainPerform Order (Simplified):

Code: Select all

<!-- Check if already docked -->
<do_if value="(this.ship.dock and (this.ship.dock.container == $destination)) or 
              (this.ship.parkedat and ((this.ship.parkedat == $destination) or 
               this.ship.parkedat.hascontext.{$destination}))">
  <debug_text text="Already docked - proceeding to training"/>
  <resume label="perform_training"/>
</do_if>

<!-- NOT DOCKED: Create DockAndWait -->
<do_if value="not $cannotdock">
  <debug_text text="Not docked. Creating DockAndWait and waiting for completion"/>
  <create_order id="'DockAndWait'" object="this.ship" immediate="true">
    <param name="destination" value="$destination"/>
    <param name="timeout" value="0s"/>
    <param name="waittime" value="60min"/>
    <param name="callerid" value="this.assignedcontrolled.order"/>
    <param name="internalorder" value="true"/>
  </create_order>
  <wait exact="1ms"/>
  <resume label="perform_training"/>
</do_if>

<label name="perform_training"/>
<!-- Training logic here -->
🔎 What We've Tried

✅ Pattern Variations:
• Exact vanilla order.trade.perform.xml pattern with <return/>
• Repair order pattern with <wait exact="1ms"/>
• Both immediate="true" and without immediate flag

✅ Parameter Combinations:
timeout="null" (infinite wait)
timeout="0s" (no timeout)
waittime="60min" (explicit wait time)
• With/without callerid parameter
• With/without undockhandler parameter

✅ Order Management:
• Proper subordinate order cleanup
• Critical order state management
• Signal-based completion handling

❓ Key Questions for the Community

1. What could cause a DockAndWait order to be cancelled immediately after successful docking?

The ship docks, DockAndWait reports "waiting" state, but then undocks 8 seconds later. What mechanism could cause this?

2. Are there hidden requirements for DockAndWait orders created by custom scripts?

Vanilla orders like TradePerform use DockAndWait successfully. Are there differences in how custom scripts should create these orders?

3. Could there be interference between the parent TrainPerform order and the DockAndWait sub-order?

Is there a proper way to coordinate between a parent AI script and its DockAndWait sub-orders?

4. What's the correct pattern for "dock and wait for script to continue" behavior?

Should custom scripts use <return/> (like TradePerform) or <wait> (like Repair) after creating DockAndWait?

🎯 Reproducible Test Case

📥 DOWNLOAD TEST MOD: Test Scripts (OneDrive)

The download contains a complete test mod that reproduces this issue:

To test:
1. Download and extract to X4/extensions/
2. Load any save game
3. Assign "DoSomething" order to any small ship
4. Watch debug log for dock/undock loop

Expected behavior: Ship docks and stays docked for training
Actual behavior: Ship docks, immediately undocks, infinite loop

🖥️ System Info
X4 Version: 7.60
Other Mods: None (vanilla + test mod only)
Script Type: AI Script (aiscripts/), not MD script

💡 Community Input Needed

This issue has been persistent across multiple implementation attempts. Any insights into:
• X4's order lifecycle management
• Proper DockAndWait usage patterns
• Debugging approaches for order cancellation
• Alternative patterns for dock-and-continue behavior

Would be incredibly helpful! The complete test case should make it easy to reproduce and investigate.

Thanks in advance for any help! 🚀

Note: Test mod ZIP attached - contains complete reproducible case with comprehensive logging
keine
staeuber
Posts: 33
Joined: Tue, 8. Feb 05, 00:36
x4

Re: Persistent DockAndWait Order Cancellation Issue

Post by staeuber »

ok now i found the issue and i was looking at the completetly wrong place

orders.base was the culprit. it has a list of orderIDs which are allowed to be processed while the ship is docked

i created a patch/DIFF and now the order gets processed without undocking.

Code: Select all

<?xml version="1.0" encoding="utf-8"?>
<diff>
  <replace sel="//aiscript[@name='orders.base']/attention/actions/do_if[contains(@value, '$object.isclass.ship and ($object.dock or $object.parkedat)')]">
    <do_if value="$object.isclass.ship and ($object.dock or $object.parkedat) and ($object.order.id != 'Undock') and ($object.order.id != 'TransferWorkers')">
      <debug_text text="'[ORDERS-BASE-PATCH] === DOCKED SHIP CHECK === Ship: %s (%s), Order: %s'.[@$object.idcode, @$object.knownname, @$object.order.id]" chance="if $object.isplayerowned then 100 else 0"/>
      
      <!-- NB: remove releasesignal params in Undock calls if we remove the undock-specific requirement to tell a ship to proceed. -->
      <do_if value="(not $object.dock or (if $object.isunit then $object.dock.isunitdockingallowed else $object.dock.isdockingallowed))
                      and ($object.order.trade
                        or $object.order.build
                        or ($object.order.id == 'DockAt')
                        or ($object.order.id == 'DockAndWait')
                        or ($object.order.id == 'TestTrainPerform')
                        or ((($object.order.id == 'Wait') or ($object.order.id == 'WaitForSignal')) and $object.order.$allowdocked and not $object.iscapitalship and not @$object.dock.container.isclass.buildstorage)
                        or (($object.order.id == 'WaitForSignal') and $object.order.$allowparked)
                        or ($object.order.id == 'Repair')
                        or ($object.order.id == 'Equip')
                        or ($object.order.id == 'Recycle')
                        or ($object.order.id == 'Escort')
                        or ($object.order.id == 'Assist')
                        or ($object.order.id == 'Resupply')
                        or ($object.order.id == 'Player_DockToTrade')
                        or ($object.order.id == 'AssignCommander')
                        or ($object.order.id == 'DepositInventory')
                        or ($object.order.id == 'Venture')
                        or ($object.order.id == 'WaitForVenture')
                        or ($object.order.id == 'MoveDie'))">
        
        <debug_text text="'[ORDERS-BASE-PATCH] ✅ Order %s is in ALLOWED list - no undocking needed'.[@$object.order.id]" chance="if $object.isplayerowned then 100 else 0"/>
        
        <!-- Log TestTrainPerform specific success -->
        <do_if value="$object.order.id == 'TestTrainPerform'">
          <debug_text text="'[ORDERS-BASE-PATCH] ✅ TestTrainPerform is now ALLOWED to stay docked!'" chance="if $object.isplayerowned then 100 else 0"/>
        </do_if>
        
        <do_if value="$object.dock">
          <set_value name="$parkingcontainer" exact="$object.dock.container"/>
        </do_if>
        <do_elseif value="$object.parkedat">
          <do_if value="$object.parkedat.container">
            <set_value name="$parkingcontainer" exact="$object.parkedat.container"/>
          </do_if>
          <do_else>
            <set_value name="$parkingcontainer" exact="$object.parkedat"/>
          </do_else>
        </do_elseif>

        <do_if value="$object.order.trade and @$object.order.trade.exchangepartner.{$object}.isoperational">
          <debug_text text="'[ORDERS-BASE-PATCH] Checking trade conditions...'" chance="if $object.isplayerowned then 100 else 0"/>
          <!-- we have to undock to trade if: -->
          <debug_text text="'condition 3: %s, loccontainer: %s, loccontainer is not exchangepartner: %s, has no common context: %s'.[($object.container and ($object.container != $object.order.trade.exchangepartner.{$object}) and not $object.order.trade.exchangepartner.{$object}.hascontext.{$object.container}), $object.container, ($object.container != $object.order.trade.exchangepartner.{$object}), not $object.order.trade.exchangepartner.{$object}.hascontext.{$object.container}]" chance="0"/>
          <!-- we are docked, -->
          <do_if value="$object.dock">
            <!-- we are not docked at exchangepartner,
              and exchangepartner is not docked at us,
              and we and exchangepartner are not docked at the same container -->
            <do_if value="not $object.hascontext.{$object.order.trade.exchangepartner.{$object}} and not $object.order.trade.exchangepartner.{$object}.hascontext.{$object} and not $object.order.trade.exchangepartner.{$object}.hascontext.{$object.container}">
              <debug_text text="'[ORDERS-BASE-PATCH] ❌ UNDOCK TRIGGER: Trade - not at exchangepartner'" chance="if $object.isplayerowned then 100 else 0"/>
              <set_value name="$undock"/>
            </do_if>
            <!-- or we are trading with a station and our current dock is not equipped for trades. -->
            <do_elseif value="not $object.order.trade.isshiptoship and not @$object.dock.istradingallowed">
              <debug_text text="'[ORDERS-BASE-PATCH] ❌ UNDOCK TRIGGER: Trade - dock not trading allowed'" chance="if $object.isplayerowned then 100 else 0"/>
              <set_value name="$undock"/>
            </do_elseif>
          </do_if>
          <!-- or we are parked at exchangepartner, (currently only used by capship to capship ware exchange) -->
          <do_elseif value="$object.parkedat and ($parkingcontainer != $object.order.trade.exchangepartner.{$object})">
            <debug_text text="'[ORDERS-BASE-PATCH] ❌ UNDOCK TRIGGER: Trade - parked not at exchangepartner'" chance="if $object.isplayerowned then 100 else 0"/>
            <set_value name="$undock"/>
          </do_elseif>
        </do_if>
        <do_elseif value="$object.order.build and ($object.order.build.object != $parkingcontainer or ($parkingcontainer.isclass.station and not @$object.dock.isbuildingallowed))">
          <debug_text text="'[ORDERS-BASE-PATCH] ❌ UNDOCK TRIGGER: Build - wrong container or dock not building allowed'" chance="if $object.isplayerowned then 100 else 0"/>
          <set_value name="$undock"/>
        </do_elseif>
        <do_elseif value="($object.order.id == 'DockAt' or $object.order.id == 'DockAndWait') and $object.isunit and $object.commander and (@$object.dock.container == $object.commander) and ($object.order.$destination == $object.commander)">
          <debug_text text="'[ORDERS-BASE-PATCH] Unit dock collection case'" chance="if $object.isplayerowned then 100 else 0"/>
          <!-- defence drone is docked at its owner and has an order dock and wait where it's docked. can happen if a shoal of drones is launched and the commander of the shoal either docks or dies before all subordinates have launched. -->
          <debug_text text="'unit %s %s %s is docked at %s %s %s and has orders to dock at it. collecting.'.[$object.idcode, $object.knownname, $object, $object.dock.container.idcode, $object.dock.container.knownname, $object.dock.container]" chance="0"/>
          <do_if value="$object.commander.units.{$object.unitcategory}.count gt $object.commander.availableunits.{$object.unitcategory}.count">
            <collect_unit object="$object.commander" unit="$object"/>
          </do_if>
          <do_else>
            <destroy_object object="$object" explosion="false"/>
          </do_else>
          <wait sinceversion="1"/>
        </do_elseif>
        <do_elseif value="($object.order.id == 'DockAt' or $object.order.id == 'DockAndWait') 
                  and (($object.order.$destination != $parkingcontainer) 
                    or ($object.dock.exists 
                      and (($object.order.$ventureplatform != $object.dock.ventureplatform) 
                        or ($object.order.$building and $parkingcontainer.isclass.station and not @$object.dock.isbuildingallowed) 
                        or ($object.order.$trading and $parkingcontainer.isclass.station and not @$object.dock.istradingallowed) 
                        or (not $object.order.$allowplayeronly and $object.dock.isplayeronly))))">
          <debug_text text="'[ORDERS-BASE-PATCH] ❌ UNDOCK TRIGGER: DockAt/DockAndWait - destination mismatch or dock capability issue'" chance="if $object.isplayerowned then 100 else 0"/>
          <set_value name="$undock"/>
        </do_elseif>
        <do_elseif value="$object.order.id == 'Player_DockToTrade' and ($object.order.$destination != $parkingcontainer or not $object.dock.istradingallowed)">
          <debug_text text="'[ORDERS-BASE-PATCH] ❌ UNDOCK TRIGGER: Player_DockToTrade'" chance="if $object.isplayerowned then 100 else 0"/>
          <set_value name="$undock"/>
        </do_elseif>
        <do_elseif value="$object.order.id == 'Repair' and $object.order.$destination != $parkingcontainer">
          <debug_text text="'[ORDERS-BASE-PATCH] ❌ UNDOCK TRIGGER: Repair - wrong destination'" chance="if $object.isplayerowned then 100 else 0"/>
          <set_value name="$undock"/>
        </do_elseif>
        <!-- undock handling for non-carrier-based ships that are docked at their commander moved to order.fight.escort to deal with carrier vs non-carrier differentiation. -->
        <do_elseif value="$object.order.id == 'Escort' and (not $object.allcommanders.indexof.{$parkingcontainer} or $object.parkedat)">
          <debug_text text="'[ORDERS-BASE-PATCH] ❌ UNDOCK TRIGGER: Escort'" chance="if $object.isplayerowned then 100 else 0"/>
          <set_value name="$undock"/>
        </do_elseif>
        <do_elseif value="$object.order.id == 'Assist' and @$object.order.$baseorder.id != 'DockAndWait'">
          <debug_text text="'[ORDERS-BASE-PATCH] ❌ UNDOCK TRIGGER: Assist'" chance="if $object.isplayerowned then 100 else 0"/>
          <set_value name="$undock"/>
        </do_elseif>
        <!-- NB: the Resupply script is an evaluation script. all actions are done by orders that Resupply add. so you don't have to move until after resupply is done, and in case of checks after just finishing a part of Resupply (such as Repair), further resupply actions will likely be done at the same station. -->
        <do_elseif value="$object.order.id == 'Resupply'">
          <debug_text text="'[ORDERS-BASE-PATCH] Resupply - no undock needed'" chance="if $object.isplayerowned then 100 else 0"/>
          <!-- do not undock -->
        </do_elseif>
        <do_elseif value="($object.order.id == 'DepositInventory') 
                   and (($object.order.$destination and ($object.order.$destination != $parkingcontainer)) 
                    or (not $object.order.$destination and ($parkingcontainer != $object.trueowner.headquarters)))">
          <debug_text text="'[ORDERS-BASE-PATCH] ❌ UNDOCK TRIGGER: DepositInventory'" chance="if $object.isplayerowned then 100 else 0"/>
          <debug_text text="'destination: %s %s %s, parkingcontainer: %s %s %s, parkingcontainer == headquarters: %s'.[@$object.order.$destination.idcode, @$object.order.$destination.knownname, $object.order.$destination, @$parkingcontainer.idcode, @$parkingcontainer.knownname, $parkingcontainer, $parkingcontainer == this.trueowner.headquarters]" chance="0"/>
          <set_value name="$undock"/>
        </do_elseif>
        <do_elseif value="($object.order.id == 'MoveDie') and (not $object.order.$atstation or not $object.hascontext.{$object.order.$atstation})">
          <debug_text text="'[ORDERS-BASE-PATCH] ❌ UNDOCK TRIGGER: MoveDie'" chance="if $object.isplayerowned then 100 else 0"/>
          <set_value name="$undock"/>
        </do_elseif>
        <remove_value name="$parkingcontainer"/>
      </do_if>
      <do_else>
        <debug_text text="'[ORDERS-BASE-PATCH] ❌ UNDOCK TRIGGER: Order %s NOT in allowed list or dock not suitable'.[@$object.order.id]" chance="if $object.isplayerowned then 100 else 0"/>
        <set_value name="$undock"/>
        <set_value name="$uselaunchtubes" exact="@$object.dock.islaunchtube or (@$object.dock.isstorage and (($object.order.id == 'Lasertower') or ($object.order.id == 'Attack') or ($object.order.id == 'AttackInRange') or ($object.order.id == 'Attack_Player') or ($object.container.primarypurpose == purpose.rig)))"/>
      </do_else>
    </do_if>
  </replace>
  
  <replace sel="//aiscript[@name='orders.base']/attention/actions/do_if[@value='$undock?']">
    <do_if value="$undock?">
      <debug_text text="'[ORDERS-BASE-PATCH] ❌ CREATING UNDOCK ORDER for %s (%s) - Order: %s'.[@$object.idcode, @$object.knownname, @$object.order.id]" chance="if $object.isplayerowned then 100 else 0"/>
      <create_order object="$object" id="'Undock'" immediate="true">
        <param name="releasesignal" value="if $object.isplayerowned and not this.$skipwait? and @$object.dock.isdockingallowed then ['playerownedship_proceed'] else null"/>
        <param name="skipwait" value="this.$skipwait? or not @$object.dock.isdockingallowed"/>
        <param name="uselaunchtubes" value="@$uselaunchtubes"/>
      </create_order>
    </do_if>
  </replace>
</diff> 
it took a while to find out :oops:
keine
staeuber
Posts: 33
Joined: Tue, 8. Feb 05, 00:36
x4

Re: Persistent DockAndWait Order Cancellation Issue

Post by staeuber »

if anybody else runs into this issue here is a small patch script which adds your custom order to prevent undocking

in this case orderID = GT_Training

Code: Select all

<?xml version="1.0" encoding="utf-8"?>
<diff>
  <!-- Add targeted intervention BEFORE undock order creation to cancel undocking for our custom orders -->
  <add sel="//aiscript[@name='orders.base']/attention/actions/do_if[@value='$undock?']" pos="before">
    <!-- Cancel undocking for our custom GT_Training orders while preserving all other orders.base mechanics -->
    <do_if value="$undock and (($object.order.id == 'GT_Training') or ($object.order.id == 'DockAndWait' and @$object.order.$callerid.id == 'GT_Training'))">
      <debug_text text="'[ORDERS-BASE-PATCH] ✅ Canceling undock for GT_Training order - %s will stay docked'.[@$object.order.id]" chance="if $object.isplayerowned then 100 else 0"/>
      <remove_value name="$undock"/>
    </do_if>
  </add>
</diff> 
keine
staeuber
Posts: 33
Joined: Tue, 8. Feb 05, 00:36
x4

Re: Persistent DockAndWait Order Cancellation Issue

Post by staeuber »

X4 Custom Orders and Undocking Issue - Complete Technical Guide

Date: June 2025
Status: Complete Technical Reference
Scope: Custom AI Orders, DockAndWait Integration, Undocking Prevention



📋 Executive Summary

This guide provides a comprehensive technical reference for X4 modders working with custom AI orders that require docking and waiting behavior. It documents the persistent undocking issue where ships dock successfully but immediately undock, and provides proven solutions.

Key Discoveries:
  • Root Cause: X4's orders.base script actively undocks ships that don't meet specific order criteria
  • Core Issue: Custom orders are not recognized in X4's allowed orders list
  • Solution: Targeted intervention in orders.base to prevent undocking for custom orders
  • Critical Insight: DockAndWait sub-orders inherit caller ID, requiring dual-condition checking


🔍 Problem Analysis

The Undocking Loop Phenomenon
Symptom: Ships dock successfully at stations but immediately undock, creating infinite dock/undock loops.

Manifestation:

Code: Select all

[Scripts] 290.40: Ship creates DockAndWait order
[Scripts] 380.23: Ship successfully docks at station
[Scripts] 380.23: DockAndWait confirms "waiting" state
[Scripts] 388.44: Ship undocks mysteriously
[Scripts] 388.44: Custom order restarts, detects not docked
[Scripts] 388.44: Creates new DockAndWait order
[INFINITE LOOP]
Root Cause Discovery
X4's orders.base Script: The game's core orders.base script runs continuously for all ships and actively manages docking behavior. It contains logic that undocks ships unless they meet specific criteria.

Critical Code Section (from aiscripts/orders.base.xml):

Code: Select all

<!-- X4's allowed orders for staying docked -->
<set_value name="$allowedorders" exact="[
  'DockAndWait', 'SupplyFleet', 'Trade', 'Build', 'Repair', 
  'Escort', 'Resupply', 'Restock'
]"/>

<!-- If ship's order is NOT in allowed list → UNDOCK -->
<do_if value="not $allowedorders.indexof.{$object.order.id}">
  <set_value name="$undock" exact="true"/>
</do_if>
The Problem: Custom orders (like GT_Training, TestTrainPerform, etc.) are not in X4's hardcoded allowed orders list, so ships get undocked immediately after docking.



🛠️ Technical Deep Dive

X4's Order Hierarchy System
Understanding X4's order system is crucial for solving the undocking issue:

Code: Select all

Main Order: GT_Training (Your custom training order)
├── Creates Sub-Order: DockAndWait (X4's built-in docking order)
│   ├── callerid: GT_Training
│   └── purpose: Handle docking mechanics
The Caller ID Chain
Critical Discovery: When custom orders create DockAndWait sub-orders, the DockAndWait order gets a callerid parameter pointing back to the parent order.

Code: Select all

<!-- Custom order creates DockAndWait -->
<create_order id="'DockAndWait'" object="this.ship" immediate="true">
  <param name="destination" value="$destination"/>
  <param name="callerid" value="this.assignedcontrolled.order"/>
  <!-- callerid now points to GT_Training order -->
</create_order>
Result: X4's orders.base sees:
  • Main order: GT_Training (not in allowed list)
  • Sub-order: DockAndWait (in allowed list, but has custom caller)
Dual-Condition Problem
X4's orders.base must handle both scenarios:
  1. Direct Custom Orders: Ship's main order is custom (e.g., GT_Training)
  2. DockAndWait with Custom Caller: Ship's order is DockAndWait but caller is custom


🔧 Solution Architecture

Targeted Intervention Approach
Strategy: Modify X4's orders.base script to recognize custom orders and prevent undocking.

Key Principles:
  • Surgical Modification: Minimal changes to preserve X4's core functionality
  • Dual-Condition Handling: Address both direct custom orders and DockAndWait with custom callers
  • Mod Compatibility: Use additive patches that don't conflict with other mods
Working Solution Implementation

File: extensions/[ModName]/aiscripts/orders.base.xml

Code: Select all

<?xml version="1.0" encoding="utf-8"?>
<diff>
  <!-- Prevent undocking for custom training orders -->
  <add sel="//aiscript[@name='orders.base']/attention/actions/do_if[@value='$undock?']" pos="before">
    <!-- Check for custom orders that should stay docked -->
    <do_if value="$undock and (($object.order.id == 'GT_Training') or ($object.order.id == 'DockAndWait' and @$object.order.$callerid.id == 'GT_Training'))">
      <!-- Cancel undocking for GT_Training orders -->
      <remove_value name="$undock"/>
      <debug_text text="'[ORDERS.BASE PATCH] Preventing undock for GT_Training order on ship: ' + $object.knownname" chance="100"/>
    </do_if>
  </add>
</diff>
Solution Breakdown

1. Patch Location:

Code: Select all

<add sel="//aiscript[@name='orders.base']/attention/actions/do_if[@value='$undock?']" pos="before">
  • Target: Just before X4's undocking decision point
  • Timing: After X4 determines undocking but before executing it
  • Method: Additive patch (doesn't modify existing code)
2. Dual-Condition Check:

Code: Select all

<do_if value="$undock and (($object.order.id == 'GT_Training') or ($object.order.id == 'DockAndWait' and @$object.order.$callerid.id == 'GT_Training'))">
  • Condition 1: $object.order.id == 'GT_Training' (direct custom order)
  • Condition 2: $object.order.id == 'DockAndWait' and @$object.order.$callerid.id == 'GT_Training' (DockAndWait with custom caller)
  • Safety: Only triggers when X4 has already decided to undock ($undock is true)
3. Undocking Prevention:

Code: Select all

<remove_value name="$undock"/>
  • Effect: Removes the undocking flag, preventing undocking
  • Preservation: All other orders.base logic continues normally
  • Scope: Only affects the specific custom orders listed


🧪 Debugging Methodology

Essential Debug Patches
To understand the undocking issue, comprehensive logging is crucial:

DockAndWait Debug Patch

Code: Select all

<!-- File: extensions/[ModName]/diff/aiscripts/order.dock.wait.xml -->
<?xml version="1.0" encoding="utf-8"?>
<diff>
  <add sel="//aiscript[@name='order.dock.wait']/attention/actions" pos="after">
    <debug_text text="'[DOCK-WAIT-PATCH] Ship: ' + this.ship.knownname + ' successfully docked at ' + $destination.knownname + '. Now waiting for caller to complete operations...'" chance="100"/>
  </add>
</diff>
Undock Debug Patch

Code: Select all

<!-- File: extensions/[ModName]/diff/aiscripts/move.undock.xml -->
<?xml version="1.0" encoding="utf-8"?>
<diff>
  <add sel="//aiscript[@name='move.undock']/attention/actions" pos="before">
    <debug_text text="'[UNDOCK-PATCH] Ship: ' + this.ship.knownname + ' is undocking. Current order: ' + this.ship.order.id + ', Caller: ' + @this.ship.order.$callerid.id" chance="100"/>
  </add>
</diff>
Orders.base Debug Patch

Code: Select all

<!-- File: extensions/[ModName]/diff/aiscripts/orders.base.xml -->
<?xml version="1.0" encoding="utf-8"?>
<diff>
  <add sel="//aiscript[@name='orders.base']/attention/actions/do_if[@value='$undock?']" pos="before">
    <do_if value="$undock">
      <debug_text text="'[ORDERS.BASE] Ship: ' + $object.knownname + ' will undock. Order: ' + $object.order.id + ', Caller: ' + @$object.order.$callerid.id" chance="100"/>
    </do_if>
  </add>
</diff>
Log Analysis Patterns
Successful Debug Session:

Code: Select all

[Scripts] 290.40: [CUSTOM-ORDER] Creating DockAndWait order
[Scripts] 380.23: [DOCK-WAIT-PATCH] Ship successfully docked, waiting for caller
[Scripts] 385.15: [ORDERS.BASE] Ship will undock. Order: DockAndWait, Caller: GT_Training
[Scripts] 385.16: [ORDERS.BASE PATCH] Preventing undock for GT_Training order
[Scripts] 420.30: [CUSTOM-ORDER] Training completed, undocking normally


📚 Implementation Patterns

Custom Order Best Practices

1. Proper DockAndWait Creation

Code: Select all

<!-- CORRECT: Include all necessary parameters -->
<create_order id="'DockAndWait'" object="this.ship" immediate="true">
  <param name="destination" value="$destination"/>
  <param name="timeout" value="0s"/>
  <param name="waittime" value="60min"/>
  <param name="callerid" value="this.assignedcontrolled.order"/>
  <param name="internalorder" value="true"/>
  <param name="undockhandler" value="this.ship.order"/>
</create_order>
Key Parameters:
  • callerid: Links DockAndWait back to parent order
  • internalorder: Marks as sub-order
  • undockhandler: Specifies who handles undocking
  • timeout: Prevents infinite waiting
  • waittime: Maximum wait duration
2. Order Coordination Patterns

Code: Select all

<!-- Pattern 1: Return after creating DockAndWait (Trade-style) -->
<create_order id="'DockAndWait'" object="this.ship" immediate="true">
  <!-- parameters -->
</create_order>
<return/>

<!-- Pattern 2: Wait for completion (Repair-style) -->
<create_order id="'DockAndWait'" object="this.ship" immediate="true">
  <!-- parameters -->
</create_order>
<wait exact="1ms"/>
<resume label="after_docking"/>
3. Docking State Validation

Code: Select all

<!-- COMPREHENSIVE: Check multiple docking conditions -->
<do_if value="(this.ship.dock and (this.ship.dock.container == $destination)) or 
              (this.ship.parkedat and ((this.ship.parkedat == $destination) or 
               this.ship.parkedat.hascontext.{$destination}))">
  <!-- Already docked - proceed with operation -->
  <resume label="perform_operation"/>
</do_if>
Orders.base Patch Templates

Single Custom Order

Code: Select all

<do_if value="$undock and $object.order.id == 'YourCustomOrder'">
  <remove_value name="$undock"/>
  <debug_text text="'Preventing undock for YourCustomOrder'" chance="100"/>
</do_if>
Multiple Custom Orders

Code: Select all

<do_if value="$undock and ['CustomOrder1', 'CustomOrder2', 'CustomOrder3'].indexof.{$object.order.id}">
  <remove_value name="$undock"/>
  <debug_text text="'Preventing undock for custom order: ' + $object.order.id" chance="100"/>
</do_if>
DockAndWait with Custom Callers

Code: Select all

<do_if value="$undock and $object.order.id == 'DockAndWait' and ['CustomOrder1', 'CustomOrder2'].indexof.{@$object.order.$callerid.id}">
  <remove_value name="$undock"/>
  <debug_text text="'Preventing undock for DockAndWait with custom caller: ' + @$object.order.$callerid.id" chance="100"/>
</do_if>


⚠️ Common Pitfalls and Solutions

1. Missing Caller ID Check
Problem: Only checking main order ID, missing DockAndWait sub-orders

Code: Select all

<!-- INCOMPLETE: Misses DockAndWait sub-orders -->
<do_if value="$undock and $object.order.id == 'CustomOrder'">
Solution: Include dual-condition check

Code: Select all

<!-- COMPLETE: Handles both scenarios -->
<do_if value="$undock and (($object.order.id == 'CustomOrder') or ($object.order.id == 'DockAndWait' and @$object.order.$callerid.id == 'CustomOrder'))">
2. Incorrect Patch Positioning
Problem: Adding patch after undocking decision is made

Code: Select all

<!-- WRONG: Too late in the process -->
<add sel="//aiscript[@name='orders.base']/attention/actions" pos="after">
Solution: Add before undocking check

Code: Select all

<!-- CORRECT: Before undocking decision -->
<add sel="//aiscript[@name='orders.base']/attention/actions/do_if[@value='$undock?']" pos="before">
3. Unsafe Property Access
Problem: Not using @ prefix for optional properties

Code: Select all

<!-- UNSAFE: Can crash if callerid doesn't exist -->
<do_if value="$object.order.$callerid.id == 'CustomOrder'">
Solution: Use safe property access

Code: Select all

<!-- SAFE: Returns null if property doesn't exist -->
<do_if value="@$object.order.$callerid.id == 'CustomOrder'">
4. Overly Broad Patches
Problem: Preventing undocking for all orders

Code: Select all

<!-- DANGEROUS: Breaks X4's core functionality -->
<do_if value="$undock">
  <remove_value name="$undock"/>
</do_if>
Solution: Target specific custom orders only

Code: Select all

<!-- SAFE: Only affects specific custom orders -->
<do_if value="$undock and $object.order.id == 'SpecificCustomOrder'">
  <remove_value name="$undock"/>
</do_if>


🔄 Alternative Approaches Evaluated

1. Early Exit Strategy ❌ FAILED
Approach: Exit orders.base early for custom orders

Code: Select all

<do_if value="$object.order.id == 'CustomOrder'">
  <return/>
</do_if>
Problems:
  • Bypassed critical ship behaviors (trade, build, escort, resupply)
  • Broke fleet coordination and resource management
  • Required <resume label="loop"/> instead of <return/>
2. Allowed Orders List Modification ❌ MOD COMPATIBILITY ISSUES
Approach: Add custom orders to X4's allowed orders list

Code: Select all

<replace sel="//set_value[@name='$allowedorders']/@exact">
  ['DockAndWait', 'SupplyFleet', 'Trade', 'Build', 'Repair', 'Escort', 'Resupply', 'Restock', 'CustomOrder']
</replace>
Problems:
  • Conflicts with other mods modifying the same list
  • Requires complete list replacement
  • Fragile to X4 updates
3. Internal Docking Logic ⚠️ COMPLEX
Approach: Handle docking internally without DockAndWait

Code: Select all

<move_to destination="$destination"/>
<dock object="$destination"/>
<wait exact="$duration"/>
<undock/>
Problems:
  • No flight path visualization in UI
  • Complex error handling required
  • Doesn't integrate with X4's order system


📊 Performance Considerations

Patch Performance Impact
Measurement: Orders.base runs continuously for all ships

Optimizations:

Code: Select all

<!-- EFFICIENT: Check undock flag first -->
<do_if value="$undock and $object.order.id == 'CustomOrder'">

<!-- INEFFICIENT: Always evaluate complex conditions -->
<do_if value="($object.order.id == 'CustomOrder') and $undock">
Best Practices:
  • Always check $undock first (short-circuit evaluation)
  • Use specific order ID checks rather than complex pattern matching
  • Minimize debug output in production versions
  • Use @ prefix for optional property access to prevent crashes
Memory and CPU Impact
Minimal Impact: The patch adds one conditional check per ship per orders.base cycle
Frequency: Orders.base runs approximately every 1-2 seconds per ship
Overhead: Negligible for typical fleet sizes (1-50 ships)



🧪 Testing and Validation

Test Scenarios
  1. Basic Functionality: Custom order docks and stays docked
  2. Order Completion: Custom order completes and undocks normally
  3. Order Cancellation: Manual order cancellation works correctly
  4. Fleet Coordination: Multiple ships with custom orders
  5. Vanilla Compatibility: Standard X4 orders unaffected
  6. Mod Compatibility: Works alongside other order mods
Validation Checklist
  • Ship docks successfully at target station
  • Ship remains docked during custom order execution
  • Ship undocks when custom order completes
  • No infinite dock/undock loops
  • Standard X4 orders (Trade, Build, etc.) work normally
  • Debug logs show patch activation
  • No performance degradation with large fleets
Debug Log Validation
Expected Success Pattern:

Code: Select all

[Scripts] XXX.XX: [CUSTOM-ORDER] Creating DockAndWait order
[Scripts] XXX.XX: Ship successfully docks at station
[Scripts] XXX.XX: [ORDERS.BASE PATCH] Preventing undock for CustomOrder
[Scripts] XXX.XX: [CUSTOM-ORDER] Operation completed, undocking


📋 Implementation Checklist

For Mod Developers
  • Identify your custom order IDs that require docking
  • Create orders.base patch file in diff/aiscripts/orders.base.xml
  • Include both direct order and DockAndWait caller checks
  • Add debug logging for troubleshooting
  • Test with various scenarios (docked, undocked, cancelled orders)
  • Validate compatibility with other mods
  • Document the patch for users
Patch Template

Code: Select all

<?xml version="1.0" encoding="utf-8"?>
<diff>
  <!-- Prevent undocking for [YourModName] custom orders -->
  <add sel="//aiscript[@name='orders.base']/attention/actions/do_if[@value='$undock?']" pos="before">
    <do_if value="$undock and (($object.order.id == 'YourCustomOrder') or ($object.order.id == 'DockAndWait' and @$object.order.$callerid.id == 'YourCustomOrder'))">
      <remove_value name="$undock"/>
      <debug_text text="'[YourModName] Preventing undock for custom order on ship: ' + $object.knownname" chance="100"/>
    </do_if>
  </add>
</diff>


🔮 Future Considerations

X4 Game Updates
Risk: Future X4 updates might modify orders.base structure
Mitigation:
  • Use XPath selectors that are likely to remain stable
  • Monitor X4 beta releases for orders.base changes
  • Maintain fallback compatibility for older X4 versions
Mod Ecosystem
Opportunity: Standardize custom order undocking prevention
Proposal: Community-maintained list of custom order IDs
Implementation: Shared patch file or mod framework

Enhanced Integration
Future Features:
  • Dynamic custom order registration system
  • Configuration-based order management
  • Integration with Simple Menu API for user control
  • Automatic detection of custom orders requiring docking


📚 References and Resources

X4 Modding Documentation Related Mods and Examples
  • TaterTrader: Advanced trading AI with custom orders
  • Variety and Rebalance Overhaul (VRO): Extensive AI modifications
  • Foundation of Conquest and War: Custom military orders
Technical References
  • aiscripts.xsd: X4 AI script schema definition
  • orders.base.xml: Core X4 order management script
  • order.dock.wait.xml: Standard docking and waiting behavior


🎯 Conclusion

The X4 custom orders undocking issue is a well-understood problem with proven solutions. The key insights are:
  1. Root Cause: X4's orders.base actively manages docking and undocks ships with unrecognized orders
  2. Solution: Targeted intervention in orders.base to prevent undocking for specific custom orders
  3. Implementation: Dual-condition checking for both direct custom orders and DockAndWait sub-orders
  4. Best Practice: Surgical patches that preserve X4's core functionality while enabling custom behavior
This guide provides the complete technical foundation for implementing reliable custom orders that require docking behavior in X4: Foundations.



Document Version: 1.0
Last Updated: June 2025
Status: Complete Technical Reference
Validation: Tested with GT_Training custom order implementation

This document represents the collective knowledge gained through extensive debugging and testing of X4 custom order systems. It should serve as the definitive reference for solving undocking issues in custom X4 mods.
Last edited by staeuber on Sun, 15. Jun 25, 18:17, edited 1 time in total.
keine
User avatar
ChemODun
Posts: 432
Joined: Mon, 12. Feb 07, 21:58
x4

Re: Persistent DockAndWait Order Cancellation Issue

Post by ChemODun »

Impressive investigation!

Thanks!
Multiply entropy by absolute zero

Freedom in space

Return to “X4: Foundations - Scripts and Modding”