File size: 4,186 Bytes
7795724
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1d3de83
7795724
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1d3de83
 
 
7795724
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1d3de83
 
 
7795724
 
 
 
 
 
 
1d3de83
 
 
7795724
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1d3de83
 
 
7795724
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
//! Frame-level synchronization for debugging.
//!
//! Provides cooperative pause/resume at frame boundaries. The game thread
//! calls [`on_frame_start`] from the TurnManager hook; when paused, it blocks
//! on a Windows event until the debug server signals it to continue.
//!
//! All control functions are safe to call from any thread.

use core::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::atomic::AtomicUsize;

use crate::log_line;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::System::Threading::{
    CreateEventA, ResetEvent, SetEvent, WaitForSingleObject,
};

/// True when the game thread should block at the next frame boundary.
static PAUSED: AtomicBool = AtomicBool::new(false);

/// Frames remaining before auto-pausing (for `step N`). -1 = inactive.
static STEP_REMAINING: AtomicI32 = AtomicI32::new(-1);

/// Frame number to auto-pause at. -1 = no breakpoint.
static BREAK_FRAME: AtomicI32 = AtomicI32::new(-1);

/// Last frame number seen by on_frame_start.
static CURRENT_FRAME: AtomicI32 = AtomicI32::new(-1);

/// Manual-reset event handle stored as usize (HANDLE = *mut c_void).
static RESUME_EVENT: AtomicUsize = AtomicUsize::new(0);

fn event() -> HANDLE {
    RESUME_EVENT.load(Ordering::Relaxed) as HANDLE
}

/// Initialize the sync primitives. Call once from DLL init.
pub fn init() {
    let handle = unsafe { CreateEventA(core::ptr::null(), 1, 1, core::ptr::null()) };
    assert!(!handle.is_null(), "debug_sync: CreateEventA failed");
    RESUME_EVENT.store(handle as usize, Ordering::Relaxed);

    // Check for OPENWA_BREAK_FRAME env var
    if let Ok(val) = std::env::var("OPENWA_BREAK_FRAME") {
        if let Ok(frame) = val.parse::<i32>() {
            BREAK_FRAME.store(frame, Ordering::Relaxed);
            let _ = log_line(&format!("[DebugSync] Breakpoint set at frame {frame}"));
        }
    }
}

/// Called from TurnManager_ProcessFrame hook at the START of each frame.
/// Blocks the game thread if paused, breakpoint hit, or stepping completed.
pub fn on_frame_start(frame: i32) {
    CURRENT_FRAME.store(frame, Ordering::Relaxed);

    // Check breakpoint
    let bp = BREAK_FRAME.load(Ordering::Relaxed);
    if bp >= 0 && frame >= bp {
        BREAK_FRAME.store(-1, Ordering::Relaxed); // one-shot
        let _ = log_line(&format!("[DebugSync] Breakpoint hit at frame {frame}"));
        do_suspend();
    }

    // Check step counter
    let remaining = STEP_REMAINING.load(Ordering::Relaxed);
    if remaining > 0 {
        let new = STEP_REMAINING.fetch_sub(1, Ordering::Relaxed) - 1;
        if new <= 0 {
            STEP_REMAINING.store(-1, Ordering::Relaxed);
            let _ = log_line(&format!("[DebugSync] Step complete at frame {frame}"));
            do_suspend();
        }
    }

    // Block if paused
    if PAUSED.load(Ordering::Acquire) {
        unsafe {
            WaitForSingleObject(event(), 0xFFFFFFFF);
        }
    }
}

/// Pause the game at the next frame boundary.
pub fn suspend() {
    do_suspend();
    let _ = log_line(&format!(
        "[DebugSync] Suspended at frame {}",
        CURRENT_FRAME.load(Ordering::Relaxed)
    ));
}

/// Resume the game.
pub fn resume() {
    PAUSED.store(false, Ordering::Release);
    STEP_REMAINING.store(-1, Ordering::Relaxed);
    unsafe {
        SetEvent(event());
    }
    let _ = log_line("[DebugSync] Resumed");
}

/// Advance `count` frames, then pause again.
pub fn step(count: i32) {
    STEP_REMAINING.store(count.max(1), Ordering::Relaxed);
    PAUSED.store(false, Ordering::Release);
    unsafe {
        SetEvent(event());
    }
}

/// Set a frame breakpoint. -1 to clear.
pub fn set_breakpoint(frame: i32) {
    BREAK_FRAME.store(frame, Ordering::Relaxed);
}

/// Query current frame number.
pub fn current_frame() -> i32 {
    CURRENT_FRAME.load(Ordering::Relaxed)
}

/// Query pause state.
pub fn is_paused() -> bool {
    PAUSED.load(Ordering::Relaxed)
}

/// Query current breakpoint. -1 = none.
pub fn breakpoint() -> i32 {
    BREAK_FRAME.load(Ordering::Relaxed)
}

// ── Internal ──

fn do_suspend() {
    PAUSED.store(true, Ordering::Release);
    unsafe {
        ResetEvent(event());
    }
}