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:
- Direct Custom Orders: Ship's main order is custom (e.g., GT_Training)
- 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:
- 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
- Basic Functionality: Custom order docks and stays docked
- Order Completion: Custom order completes and undocks normally
- Order Cancellation: Manual order cancellation works correctly
- Fleet Coordination: Multiple ships with custom orders
- Vanilla Compatibility: Standard X4 orders unaffected
- 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:
- Root Cause: X4's orders.base actively manages docking and undocks ships with unrecognized orders
- Solution: Targeted intervention in orders.base to prevent undocking for specific custom orders
- Implementation: Dual-condition checking for both direct custom orders and DockAndWait sub-orders
- 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.