1use std::collections::HashMap;
6use std::mem::size_of;
7use std::cell::RefCell;
8use std::process::Child;
9use anyhow::bail;
10use sysinfo::{Pid, ProcessExt, System, SystemExt, PidExt};
11use crate::models::{StatsBlockWithFrames, StatsDataBlock, StatsFrame};
12
13use self::proc_mem_wrapper::Handle;
14
15pub mod proc_mem_wrapper;
16
17const DATA_BLOCK_SIZE: usize = size_of::<StatsDataBlock>();
21const STATS_FRAME_SIZE: usize = size_of::<StatsFrame>();
22
23thread_local! {
24 static BLOCK_BUF: RefCell<[u8; DATA_BLOCK_SIZE]> = RefCell::new([0_u8; DATA_BLOCK_SIZE]);
25 static FRAME_BUF: RefCell<[u8; STATS_FRAME_SIZE]> = RefCell::new([0_u8; STATS_FRAME_SIZE]);
26 static SYSTEM: RefCell<System> = RefCell::new(System::new_all());
27}
28
29pub struct OsInfo {
33 pub default_block_marker: usize,
34 pub default_process_name: String,
35 pub can_create_child: bool,
36 pub offsets: HashMap<String, Vec<usize>>
37}
38
39#[derive(Debug)]
40pub enum OperatingSystem {
41 Linux,
42 LinuxProton,
43 Windows
44}
45
46pub struct GameConnection {
47 pub pid: Pid,
48 pub path: String,
49 pub handle: Handle,
50 pub base_address: usize,
51 pub last_fetch: Option<StatsBlockWithFrames>,
52 pub child_handle: Option<Child>,
53 pub params: ConnectionParams,
54 pub(crate) pointers: Pointers
55}
56
57#[derive(Default)]
58pub struct Pointers {
59 pub ddstats_block: Option<usize>,
60 pub base_address: Option<usize>
61}
62
63#[derive(Default)]
64pub struct MemoryOverride {
65 pub block_marker: Option<usize>,
66 pub process_name: Option<String>,
67}
68
69pub struct ConnectionParams {
70 pub create_child: bool,
71 pub operating_system: OperatingSystem,
72 pub overrides: MemoryOverride,
73}
74
75#[repr(C)]
76#[derive(Debug)]
77pub struct Vec3 {
78 pub x: f32,
79 pub y: f32,
80 pub z: f32,
81}
82
83impl OsInfo {
87 pub fn get_from_os(os: &OperatingSystem) -> Self {
88 match os {
89 OperatingSystem::Linux => Self {
90 can_create_child: true,
91 default_block_marker: 0x00521C98,
92 default_process_name: String::from("devildaggers"),
93 offsets: HashMap::new()
94 },
95 OperatingSystem::Windows => Self {
96 can_create_child: false,
97 default_block_marker: 0x250DC0,
98 default_process_name: String::from("dd.exe"),
99 offsets: HashMap::new()
100 },
101 OperatingSystem::LinuxProton => Self {
102 can_create_child: false,
103 default_block_marker: 0x250DC0,
104 default_process_name: String::from("wine-preloader"),
105 offsets: HashMap::new()
106 }
107 }
108 }
109}
110
111impl ConnectionParams {
112 pub fn empty() -> Self {
113 Self {
114 create_child: false,
115 operating_system: OperatingSystem::Linux,
116 overrides: MemoryOverride::default()
117 }
118 }
119}
120
121impl GameConnection {
122 #[cfg(target_os = "windows")]
123 pub fn try_create(params: ConnectionParams) -> anyhow::Result<Self> {
124 let os_info = OsInfo::get_from_os(¶ms.operating_system);
125 let proc_name = params.overrides.process_name.as_ref().unwrap_or(&os_info.default_process_name).clone();
126 let proc = get_proc(&proc_name);
127 if proc.is_none() { anyhow::bail!("Process not found") }
128 let pid = proc.as_ref().unwrap().1;
129 let handle = Handle::new(pid.as_u32() as usize)?;
130 let base_address = base_addr(&handle, ¶ms);
131 if base_address.is_err() { anyhow::bail!("Couldn't get base address") }
132 let base_address = base_address.unwrap();
133 let ptrs = Pointers { base_address: Some(base_address), ..Default::default() };
134 Ok(Self {
135 pid,
136 handle,
137 base_address,
138 path: proc.as_ref().unwrap().0.clone(),
139 child_handle: None,
140 last_fetch: None,
141 params,
142 pointers: ptrs
143 })
144 }
145
146 #[cfg(target_os = "linux")]
147 pub fn try_create(params: ConnectionParams) -> anyhow::Result<Self> {
148 let os_info = OsInfo::get_from_os(¶ms.operating_system);
149 let proc_name = params.overrides.process_name.as_ref().unwrap_or(&os_info.default_process_name).clone();
150 let mut proc = get_proc(&proc_name);
151 if proc.is_none() { anyhow::bail!("Process not found") }
152 let mut pid = proc.as_ref().unwrap().1;
153 let mut handle = Handle::new(pid.as_u32() as usize)?;
154 let mut c = None;
155 if handle.copy_address(0, &mut [0u8]).is_err() && params.create_child{
156 c = create_as_child(pid);
157 proc = get_proc(&proc_name);
158 pid = proc.as_ref().unwrap().1;
159 handle = Handle::new(pid.as_u32() as usize)?;
160 }
161 let base_address = base_addr(&handle, ¶ms);
162 if base_address.is_err() { anyhow::bail!("Couldn't get base address") }
163 let base_address = base_address.unwrap();
164 let ptrs = Pointers { base_address: Some(base_address), ..Default::default() };
165 Ok(Self {
166 pid,
167 handle,
168 base_address,
169 path: proc.as_ref().unwrap().0.clone(),
170 child_handle: c,
171 last_fetch: None,
172 params,
173 pointers: ptrs
174 })
175 }
176
177 pub fn dead_connection() -> Self {
178 Self {
179 pid: Pid::from_u32(0),
180 base_address: 0,
181 last_fetch: None,
182 path: String::new(),
183 handle: Handle::null_type(),
184 child_handle: None,
185 params: ConnectionParams::empty(),
186 pointers: Pointers::default()
187 }
188 }
189
190 pub fn is_alive(&mut self) -> bool {
191 match self.read_stats_block() {
192 Ok(_) => true,
193 Err(_e) => false
194 }
195 }
196
197 pub fn is_alive_res(&mut self) -> anyhow::Result<()> {
198 match self.read_stats_block() {
199 Ok(_) => Ok(()),
200 Err(e) => Err(e),
201 }
202 }
203
204 pub fn read_stats_block(&mut self) -> anyhow::Result<StatsDataBlock> {
205 read_stats_data_block(&self.handle, &self.params, &mut self.pointers)
206 }
207
208 pub fn read_mem(&self, addr: usize, buffer: &mut [u8]) -> anyhow::Result<()> {
209 self.handle.copy_address(addr, buffer)
210 }
211
212 #[cfg(target_os = "windows")]
213 pub fn maximize_dd(&self) {
214 use winapi::shared::windef::HWND;
215 use winapi::shared::minwindef::DWORD;
216
217 enumerate_windows(|hwnd: HWND| {
218 let mut pid: DWORD = DWORD::default();
219 unsafe { winapi::um::winuser::GetWindowThreadProcessId(hwnd, &mut pid); }
220 if pid as u32 != self.pid.as_u32() {
221 true
222 } else {
223 unsafe { winapi::um::winuser::ShowWindow(hwnd, 9); }
224 false
225 }
226 })
227 }
228
229 #[cfg(target_os = "linux")]
230 pub fn maximize_dd(&self) {
231 }
233
234 pub fn read_stats_block_with_frames(&mut self) -> anyhow::Result<StatsBlockWithFrames> {
235 match read_stats_data_block(&self.handle, &self.params, &mut self.pointers) {
236 Ok(data) => {
237 let res = StatsBlockWithFrames {
238 frames: self.stat_frames_from_block(&data)?,
239 block: data,
240 };
241 self.last_fetch = Some(res.clone());
242 Ok(res)
243 },
244 Err(e) => {
245 log::info!("[DDCORE] Failed to read stats block {e:?}");
246 Err(anyhow::anyhow!(e))
247 }
248 }
249 }
250
251 pub fn stat_frames_from_block(
252 &mut self,
253 block: &StatsDataBlock,
254 ) -> anyhow::Result<Vec<StatsFrame>> {
255 let (mut ptr, len) = (
256 block.get_stats_pointer(),
257 block.stats_frames_loaded as usize,
258 );
259 let mut res = Vec::with_capacity(len);
260 FRAME_BUF.with(|buf| {
261 let mut buf = buf.borrow_mut();
262 for _ in 0..len {
263 self.handle.copy_address(ptr, buf.as_mut())?;
264 let (_head, body, _tail) = unsafe { buf.align_to::<StatsFrame>() };
265 res.push(body[0]);
266 ptr += STATS_FRAME_SIZE;
267 }
268 Ok(res)
269 })
270 }
271
272 pub fn replay_bin(&mut self) -> anyhow::Result<Vec<u8>> {
273 if let Some(block) = &self.last_fetch {
274 let (ptr, len) = (
275 block.block.get_replay_pointer(),
276 block.block.replay_buffer_length as usize,
277 );
278 let mut res = vec![0u8; len];
279 self.handle.copy_address(ptr, &mut res)?;
280 Ok(res)
281 } else {
282 Err(anyhow::anyhow!(std::io::Error::new(
283 std::io::ErrorKind::NotFound,
284 "Stats not available",
285 )))
286 }
287 }
288
289 pub fn stat_frames(&self) -> anyhow::Result<Vec<StatsFrame>> {
290 if let Some(last_data) = &self.last_fetch {
291 let (mut ptr, len) = (
292 last_data.block.get_stats_pointer(),
293 last_data.block.stats_frames_loaded as usize,
294 );
295 let mut res = Vec::with_capacity(len);
296 FRAME_BUF.with(|buf| {
297 let mut buf = buf.borrow_mut();
298 for _ in 0..len {
299 self.handle.copy_address(ptr, buf.as_mut())?;
300 let (_head, body, _tail) = unsafe { buf.align_to::<StatsFrame>() };
301 res.push(body[0]);
302 ptr += STATS_FRAME_SIZE;
303 }
304 Ok(res)
305 })
306 } else {
307 Err(anyhow::anyhow!(std::io::Error::new(
308 std::io::ErrorKind::NotFound,
309 "Stats not available",
310 )))
311 }
312 }
313
314 pub fn last_stat_frame(&self) -> anyhow::Result<StatsFrame> {
315 if let Some(last_data) = &self.last_fetch {
316 let (mut ptr, len) = (
317 last_data.block.get_stats_pointer(),
318 last_data.block.stats_frames_loaded as usize,
319 );
320 ptr += STATS_FRAME_SIZE * (len - 1);
321 FRAME_BUF.with(|buf| {
322 let mut buf = buf.borrow_mut();
323 self.handle.copy_address(ptr, buf.as_mut())?;
324 let (_head, body, _tail) = unsafe { buf.align_to::<StatsFrame>() };
325 Ok(body[0])
326 })
327 } else {
328 Err(anyhow::anyhow!(std::io::Error::new(
329 std::io::ErrorKind::NotFound,
330 "Stats not available",
331 )))
332 }
333 }
334
335 pub fn play_replay(&self, replay: std::sync::Arc<Vec<u8>>) -> anyhow::Result<()> {
336 if let Some(last_data) = &self.last_fetch {
337 #[cfg(feature = "logger")]
338 log::info!("[DDCORE] Attempting to load replay");
339
340 let ddstats_addr = self.pointers.ddstats_block.expect("last data can't exist without this also being set");
341 let replay_buffer_addr = last_data.block.get_replay_pointer();
342 let flag_addr = ddstats_addr + 316;
343 let len_addr = ddstats_addr + 312;
344 let len = replay.len() as i32;
345
346 #[cfg(feature = "logger")]
347 log::info!("[DDCORE] Replay Flag debug: {ddstats_addr:X} {replay_buffer_addr:X} {flag_addr:X}");
348
349 self.handle.put_address(replay_buffer_addr, &replay)?;
350 self.handle.put_address(len_addr, &len.to_le_bytes())?;
351 self.handle.put_address(flag_addr, &[true as u8])?;
352
353 Ok(())
354 } else {
355 bail!("No data found");
356 }
357 }
358}
359
360pub fn get_proc(process_name: &str) -> Option<(String, Pid)> {
364 SYSTEM.with(|s| {
365 let mut s = s.borrow_mut();
366 s.refresh_processes();
367 if let Some(process) = s.processes_by_exact_name(process_name).next() {
368 return Some((String::from(process.exe().to_str().unwrap()), process.pid()));
369 }
370 None
371 })
372}
373
374#[cfg(target_os = "windows")]
375pub fn base_addr(handle: &Handle, params: &ConnectionParams) -> anyhow::Result<usize> {
376 let os_info = OsInfo::get_from_os(¶ms.operating_system);
377 let proc_name = params.overrides.process_name.as_ref().unwrap_or(&os_info.default_process_name).clone();
378 #[cfg(feature = "logger")]
379 log::info!("[DDCORE] reading base address: {} {proc_name}", handle.pid);
380 let addr = unsafe { get_base_address(Pid::from_u32(handle.pid as u32), proc_name) };
381 #[cfg(feature = "logger")]
382 log::info!("[DDCORE] base address: {addr:?}");
383 addr
384}
385
386#[cfg(target_os = "linux")]
387pub fn base_addr(handle: &Handle, params: &ConnectionParams) -> anyhow::Result<usize> {
388 use std::io::Read;
389
390 use scan_fmt::scan_fmt;
391 let os_info = OsInfo::get_from_os(¶ms.operating_system);
392 let proc_name = params.overrides.process_name.as_ref().unwrap_or(&os_info.default_process_name).clone();
393 let pid = Pid::from_u32(handle.pid as u32);
394
395 match ¶ms.operating_system {
396 OperatingSystem::Linux => get_base_address(pid, proc_name),
397 OperatingSystem::Windows => get_base_address(pid, proc_name),
398 OperatingSystem::LinuxProton => {
399 use std::{
400 fs::File,
401 io::{BufRead, BufReader},
402 };
403
404 let mut stat = String::new();
405 BufReader::new(File::open(format!("/proc/{}/stat", pid))?).read_to_string(&mut stat)?;
406
407 if !stat.contains("dd.exe") {
408 return Err(anyhow::anyhow!(std::io::Error::new(std::io::ErrorKind::NotFound, "Not the correct process")));
409 }
410
411 let f = BufReader::new(File::open(format!("/proc/{}/maps", pid))?);
412 let mut magic_buf = [0u8; 2];
413
414 #[cfg(feature = "logger")]
415 log::info!("[DDCORE] Found LinuxProton dd.exe {:?}", pid);
416
417 for line in f.lines().flatten() {
418 if let Ok((start, _end, _perms, mod_path)) = scan_fmt!(&line, "{x}-{x} {} {*} {*} {*} {[^\t\n]}\n", [hex usize], [hex usize], String, String)
419 {
420 let r = handle.copy_address(start, &mut magic_buf);
421 if r.is_err() {
422 #[cfg(feature = "logger")]
423 log::info!("[DDCORE] Failed to read memory {:?} {} {:X}", r.err(), pid, start);
424 continue;
425 }
426
427 if mod_path.contains("dd.exe") && is_windows_exe(&magic_buf) {
428 return Ok(start);
429 }
430 }
431 }
432
433 Err(anyhow::anyhow!(std::io::Error::new(
434 std::io::ErrorKind::NotFound,
435 "No base address",
436 )))
437 }
438 }
439}
440
441pub fn is_elf(start_bytes: &[u8; 4]) -> bool {
442 let elf_signature: [u8; 4] = [0x7f, 0x45, 0x4c, 0x46];
443 elf_signature == *start_bytes
444}
445
446pub fn is_windows_exe(start_bytes: &[u8; 2]) -> bool {
447 let elf_signature: [u8; 2] = [0x4D, 0x5A];
448 elf_signature == *start_bytes
449}
450
451#[cfg(target_os = "linux")]
452pub fn get_base_address(pid: Pid, proc_name: String) -> anyhow::Result<usize> {
453 use scan_fmt::scan_fmt;
454 use std::{
455 fs::File,
456 io::{BufRead, BufReader},
457 };
458
459 let f = BufReader::new(File::open(format!("/proc/{}/maps", pid))?);
460 let handle = Handle::new(pid.as_u32() as usize)?;
461 let mut magic_buf = [0u8; 4];
462
463 for line in f.lines().flatten() {
464 if let Ok((start, _end, perms, mod_path)) = scan_fmt!(&line, "{x}-{x} {} {*} {*} {*} {[^\t\n]}\n", [hex usize], [hex usize], String, String)
465 {
466 let r = handle.copy_address(start, &mut magic_buf);
467 if r.is_err() {
468 continue;
469 }
470
471 if is_elf(&magic_buf) && mod_path.contains(&proc_name) && perms.contains('x') {
472 return Ok(start);
473 }
474 }
475 }
476
477 Err(anyhow::anyhow!(std::io::Error::new(
478 std::io::ErrorKind::NotFound,
479 "No base address",
480 )))
481}
482
483#[cfg(target_os = "windows")]
484pub fn enumerate_windows<F>(mut callback: F)
485 where F: FnMut(winapi::shared::windef::HWND) -> bool
486{
487 use winapi::shared::windef::HWND;
488 use winapi::shared::minwindef::LPARAM;
489 use winapi::um::winuser::EnumWindows;
490 use std::mem;
491 use winapi::ctypes::c_void;
492 let mut trait_obj: &mut dyn FnMut(HWND) -> bool = &mut callback;
493 let closure_pointer_pointer: *mut c_void = unsafe { mem::transmute(&mut trait_obj) };
494
495 let lparam = closure_pointer_pointer as LPARAM;
496 unsafe { EnumWindows(Some(enumerate_callback), lparam) };
497}
498
499#[allow(clippy::transmute_ptr_to_ref)]
500#[cfg(target_os = "windows")]
501unsafe extern "system" fn enumerate_callback(hwnd: winapi::shared::windef::HWND, lparam: winapi::shared::minwindef::LPARAM) -> winapi::shared::minwindef::BOOL {
502 use std::mem;
503 use winapi::shared::windef::HWND;
504 use winapi::shared::minwindef::{TRUE, FALSE};
505 use winapi::ctypes::c_void;
506 let closure: &mut &mut dyn FnMut(HWND) -> bool = mem::transmute(lparam as *mut c_void);
507 if closure(hwnd) { TRUE } else { FALSE }
508}
509
510#[cfg(target_os = "windows")]
513pub unsafe fn get_base_address(pid: Pid, _proc_name: String) -> anyhow::Result<usize> {
514 use winapi::um::handleapi::CloseHandle;
516 use std::{mem::size_of_val, os::raw::c_ulong};
517
518 let snapshot = winapi::um::tlhelp32::CreateToolhelp32Snapshot(
519 winapi::um::tlhelp32::TH32CS_SNAPMODULE | winapi::um::tlhelp32::TH32CS_SNAPMODULE32,
520 pid.as_u32() as winapi::shared::minwindef::DWORD,
521 );
522
523 let mut me: winapi::um::tlhelp32::MODULEENTRY32 = std::mem::zeroed();
524 me.dwSize = size_of_val(&me) as c_ulong as winapi::shared::minwindef::DWORD;
525 winapi::um::tlhelp32::Module32First(snapshot, &mut me);
526
527 let res = me.modBaseAddr as usize;
528 CloseHandle(snapshot);
529 Ok(res)
530}
531
532#[cfg(target_os = "windows")]
533fn _create_as_child(_pid: Pid) -> Option<Child> {
534 None
535}
536
537#[cfg(target_os = "linux")]
538fn create_as_child(pid: Pid) -> Option<Child> {
539 use std::{
540 fs::File,
541 io::{BufReader, Read},
542 path::Path,
543 process::Command,
544 };
545
546 let mut exe = String::new();
547 BufReader::new(File::open(format!("/proc/{}/cmdline", pid)).expect("Coudln't read cmdline"))
548 .read_to_string(&mut exe)
549 .unwrap();
550 let cwd = Path::new(&format!("/proc/{}/cwd", pid)).to_owned();
551 let mut exe = exe.chars();
552 exe.next_back();
553 let exe = exe.as_str();
554 Command::new("kill")
555 .arg(format!("{}", pid))
556 .spawn()
557 .expect("Couldn't kill current DD process");
558 let old_cwd = std::env::current_dir().expect("Couldn't save cwd");
559 std::env::set_current_dir(&cwd).expect("Coudln't set cwd");
560 Command::new("sh")
561 .arg("-c")
562 .arg("echo")
563 .arg("422970 > steam_appid.txt")
564 .spawn()
565 .expect("Coudln't write steam appid");
566 Command::new("nohup")
567 .arg(exe)
568 .spawn()
569 .expect("Couldn't create DD child process");
570 std::env::set_current_dir(&old_cwd).expect("Couldn't set cwd");
571 None
572}
573
574pub fn mem_search(handle: &Handle, to_find: &[u8]) -> anyhow::Result<usize> {
575 let mut big_ass_buffer = [0_u8; 1024 * 100]; let mut offset = 0x00010000;
577 loop {
578 handle.copy_address(offset, &mut big_ass_buffer)?;
579 for (i, w) in big_ass_buffer.windows(to_find.len()).enumerate() {
580 if w == to_find {
581 return Ok(offset + i);
582 }
583 }
584 offset += big_ass_buffer.len();
585 }
586}
587
588fn calc_pointer_ddstats_block(handle: &Handle, params: &ConnectionParams, base_address: usize) -> anyhow::Result<usize> {
589 let os_info = OsInfo::get_from_os(¶ms.operating_system);
590 let block_start = params.overrides.block_marker.unwrap_or(os_info.default_block_marker);
591 log::info!("[DDCORE] block start {block_start}");
592 match ¶ms.operating_system {
593 OperatingSystem::Linux => {
594 handle.get_offset(&[base_address + block_start, 0])
595 },
596 OperatingSystem::Windows => {
597 handle.get_offset(&[base_address + block_start, 0])
598 },
599 OperatingSystem::LinuxProton => {
600 mem_search(handle, b"__ddstats__")
601 }
602 }
603}
604
605pub fn read_stats_data_block(handle: &Handle, params: &ConnectionParams, pointers: &mut Pointers) -> anyhow::Result<StatsDataBlock> {
606 let base = if pointers.base_address.is_none() { base_addr(handle, params)? } else { *pointers.base_address.as_ref().unwrap() };
607 pointers.base_address = Some(base);
608 BLOCK_BUF.with(|buf| {
609 let pointer;
610 if let Some(ddstats_ptr) = pointers.ddstats_block {
611 pointer = ddstats_ptr;
612 } else {
613 pointers.ddstats_block = Some(calc_pointer_ddstats_block(handle, params, base)?);
614 pointer = *pointers.ddstats_block.as_ref().unwrap();
615 }
616 let mut buf = buf.borrow_mut();
617 handle.copy_address(pointer, buf.as_mut())?;
618 if !buf.starts_with(b"__ddstats__") {
619 return Err(anyhow::anyhow!(std::io::Error::new(std::io::ErrorKind::InvalidData, "No ddstats block found at address")));
620 }
621 let (_head, body, _tail) = unsafe { buf.as_mut().align_to::<StatsDataBlock>() };
622 Ok(body[0].clone())
623 })
624}
625
626#[cfg(target_os = "windows")]
627pub fn start_dd() -> anyhow::Result<()> {
628 use std::process::Command;
629 Command::new("cmd").arg("/c start steam://run/422970").output()?;
630 Ok(())
631}
632
633#[cfg(target_os = "linux")]
634pub fn start_dd() -> anyhow::Result<()> {
635 use std::process::Command;
636 Command::new("steam").arg("steam://run/422970").output()?;
637 Ok(())
638}
639