add Rust ScrapHacks prototype and network sniffer/parser
This commit is contained in:
		
							parent
							
								
									58407ecc9f
								
							
						
					
					
						commit
						63962c95cc
					
				
					 27 changed files with 5008 additions and 0 deletions
				
			
		
							
								
								
									
										2
									
								
								tools/remaster/scrap_net/.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								tools/remaster/scrap_net/.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | ||||||
|  | /target | ||||||
|  | /.history | ||||||
							
								
								
									
										1015
									
								
								tools/remaster/scrap_net/Cargo.lock
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1015
									
								
								tools/remaster/scrap_net/Cargo.lock
									
										
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										28
									
								
								tools/remaster/scrap_net/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								tools/remaster/scrap_net/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | ||||||
|  | [package] | ||||||
|  | name = "scrap_net" | ||||||
|  | version = "0.1.0" | ||||||
|  | edition = "2021" | ||||||
|  | authors = ["Daniel Seiller <earthnuker@gmail.com>"] | ||||||
|  | description = "Scrapland Remastered network sniffer, proxy (and soon hopefully parser)" | ||||||
|  | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
|  | 
 | ||||||
|  | [dependencies] | ||||||
|  | chacha20 = { version = "0.9", features = ["std"] } | ||||||
|  | poly1305 = { version = "0.8", features = ["std"] } | ||||||
|  | rhexdump = "0.1" | ||||||
|  | tokio = { version = "1.21", features = ["full"] } | ||||||
|  | clap = {version = "4.0", features = ["derive"]} | ||||||
|  | rand = "0.8" | ||||||
|  | dialoguer = "0.10" | ||||||
|  | binrw = "0.11" | ||||||
|  | modular-bitfield = "0.11" | ||||||
|  | hex = "0.4" | ||||||
|  | lazy_static = "1.4.0" | ||||||
|  | rustyline-async = "0.3" | ||||||
|  | futures-util = "0.3.24" | ||||||
|  | itertools = "0.10.5" | ||||||
|  | anyhow = "1.0.68" | ||||||
|  | 
 | ||||||
|  | [profile.release] | ||||||
|  | lto="fat" | ||||||
|  | opt-level = 3 | ||||||
							
								
								
									
										22
									
								
								tools/remaster/scrap_net/get_app.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								tools/remaster/scrap_net/get_app.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | ||||||
|  | from distutils.command.install_data import install_data | ||||||
|  | import winreg as reg | ||||||
|  | import vdf | ||||||
|  | from pathlib import Path | ||||||
|  | import pefile | ||||||
|  | app_id="897610" | ||||||
|  | try: | ||||||
|  |     key = reg.OpenKey(reg.HKEY_LOCAL_MACHINE,"SOFTWARE\\Valve\\Steam") | ||||||
|  | except FileNotFoundError: | ||||||
|  |     key = reg.OpenKey(reg.HKEY_LOCAL_MACHINE,"SOFTWARE\\Wow6432Node\\Valve\\Steam") | ||||||
|  | path=Path(reg.QueryValueEx(key,"InstallPath")[0]) | ||||||
|  | libraryfolders=vdf.load((path/"steamapps"/"libraryfolders.vdf").open("r"))['libraryfolders'] | ||||||
|  | for folder in libraryfolders.values(): | ||||||
|  |     path=Path(folder['path']) | ||||||
|  |     if app_id in folder['apps']: | ||||||
|  |         install_dir = vdf.load((path/"steamapps"/f"appmanifest_{app_id}.acf").open("r"))['AppState']['installdir'] | ||||||
|  |         install_dir=path/"steamapps"/"common"/install_dir | ||||||
|  |         for file in install_dir.glob("**/*.exe"): | ||||||
|  |             pe = pefile.PE(file, fast_load=True) | ||||||
|  |             entry = pe.OPTIONAL_HEADER.AddressOfEntryPoint | ||||||
|  |             if pe.get_dword_at_rva(entry) == 0xE8: | ||||||
|  |                 print(file) | ||||||
							
								
								
									
										93
									
								
								tools/remaster/scrap_net/src/hex_ii.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								tools/remaster/scrap_net/src/hex_ii.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,93 @@ | ||||||
|  | use itertools::Itertools; | ||||||
|  | use std::fmt::Display; | ||||||
|  | use std::ops::{Deref, DerefMut}; | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, PartialEq, Eq)] | ||||||
|  | enum HexII { | ||||||
|  |     Ascii(char), | ||||||
|  |     Byte(u8), | ||||||
|  |     Null, | ||||||
|  |     Full, | ||||||
|  |     Eof, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl From<&u8> for HexII { | ||||||
|  |     fn from(v: &u8) -> Self { | ||||||
|  |         match v { | ||||||
|  |             0x00 => Self::Null, | ||||||
|  |             0xFF => Self::Full, | ||||||
|  |             c if c.is_ascii_graphic() => Self::Ascii(*c as char), | ||||||
|  |             v => Self::Byte(*v), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Display for HexII { | ||||||
|  |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
|  |         match self { | ||||||
|  |             HexII::Ascii(v) => write!(f, ".{}", v)?, | ||||||
|  |             HexII::Byte(v) => write!(f, "{:02x}", v)?, | ||||||
|  |             HexII::Null => write!(f, "  ")?, | ||||||
|  |             HexII::Full => write!(f, "##")?, | ||||||
|  |             HexII::Eof => write!(f, " ]")?, | ||||||
|  |         } | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | struct HexIILine(Vec<HexII>); | ||||||
|  | 
 | ||||||
|  | impl Display for HexIILine { | ||||||
|  |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
|  |         for (i, v) in self.0.iter().enumerate() { | ||||||
|  |             if i != 0 { | ||||||
|  |                 write!(f, " ")?; | ||||||
|  |             } | ||||||
|  |             write!(f, "{}", v)?; | ||||||
|  |         } | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl From<&[u8]> for HexIILine { | ||||||
|  |     fn from(l: &[u8]) -> Self { | ||||||
|  |         Self(l.iter().map(HexII::from).collect()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Deref for HexIILine { | ||||||
|  |     type Target = Vec<HexII>; | ||||||
|  |     fn deref(&self) -> &Self::Target { | ||||||
|  |         &self.0 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl DerefMut for HexIILine { | ||||||
|  |     fn deref_mut(&mut self) -> &mut Self::Target { | ||||||
|  |         &mut self.0 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub fn hex_ii_dump<T: Iterator<Item = u8>>(data: T, base_offset: usize, total: usize) { | ||||||
|  |     const CHUNK_SIZE: usize = 0x10; | ||||||
|  |     let mut num_digits = (std::mem::size_of_val(&total) * 8) - (total.leading_zeros() as usize); | ||||||
|  |     if (num_digits % 8) != 0 { | ||||||
|  |         num_digits += 8 - (num_digits % 8) | ||||||
|  |     } | ||||||
|  |     num_digits >>= 2; | ||||||
|  |     for (mut offset, line) in data.chunks(CHUNK_SIZE).into_iter().enumerate() { | ||||||
|  |         offset += base_offset; | ||||||
|  |         let mut line = HexIILine::from(line.collect::<Vec<_>>().as_slice()); | ||||||
|  |         if line.len() < CHUNK_SIZE { | ||||||
|  |             line.push(HexII::Eof); | ||||||
|  |         } | ||||||
|  |         while line.len() < CHUNK_SIZE { | ||||||
|  |             line.push(HexII::Null); | ||||||
|  |         } | ||||||
|  |         if line.iter().all(|v| v == &HexII::Null) { | ||||||
|  |             continue; | ||||||
|  |         } | ||||||
|  |         let offset = format!("{:digits$x}", offset * CHUNK_SIZE, digits = num_digits); | ||||||
|  |         println!("{} | {:<16} |", offset, line); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										640
									
								
								tools/remaster/scrap_net/src/main.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										640
									
								
								tools/remaster/scrap_net/src/main.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,640 @@ | ||||||
|  | use anyhow::{bail, ensure, Result}; | ||||||
|  | use binrw::BinReaderExt; | ||||||
|  | use binrw::{BinRead, NullString}; | ||||||
|  | use chacha20::cipher::KeyInit; | ||||||
|  | use chacha20::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek}; | ||||||
|  | use chacha20::ChaCha20; | ||||||
|  | use clap::Parser; | ||||||
|  | use dialoguer::theme::ColorfulTheme; | ||||||
|  | use dialoguer::Select; | ||||||
|  | use futures_util::FutureExt; | ||||||
|  | use poly1305::Poly1305; | ||||||
|  | use rand::{thread_rng, Rng}; | ||||||
|  | use rhexdump::hexdump; | ||||||
|  | use rustyline_async::{Readline, ReadlineError, SharedWriter}; | ||||||
|  | use std::collections::BTreeMap; | ||||||
|  | use std::error::Error; | ||||||
|  | use std::fmt::Display; | ||||||
|  | use std::io::Cursor; | ||||||
|  | use std::io::Write; | ||||||
|  | use std::iter; | ||||||
|  | use std::net::SocketAddr; | ||||||
|  | use std::net::ToSocketAddrs; | ||||||
|  | use std::net::{IpAddr, Ipv4Addr}; | ||||||
|  | use std::path::PathBuf; | ||||||
|  | use std::time::{Duration, Instant}; | ||||||
|  | use tokio::io::AsyncBufReadExt; | ||||||
|  | use tokio::net::UdpSocket; | ||||||
|  | use tokio::time; | ||||||
|  | 
 | ||||||
|  | mod hex_ii; | ||||||
|  | mod parser; | ||||||
|  | 
 | ||||||
|  | const KEY: &[u8; 32] = b"\x02\x04\x06\x08\x0a\x0c\x0e\x10\x12\x14\x16\x18\x1a\x1c\x1e\x20\x22\x24\x26\x28\x2a\x2c\x2e\x30\x32\x34\x36\x38\x3a\x3c\x3e\x40"; | ||||||
|  | const INFO_PACKET: &[u8] = b"\x7f\x01\x00\x00\x07"; | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone)] | ||||||
|  | struct ServerFlags { | ||||||
|  |     dedicated: bool, | ||||||
|  |     force_vehicle: bool, | ||||||
|  |     _rest: u8, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Display for ServerFlags { | ||||||
|  |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
|  |         let force_vehicle = if self.force_vehicle { "F" } else { " " }; | ||||||
|  |         let dedicated = if self.dedicated { "D" } else { " " }; | ||||||
|  |         write!(f, "{}{}", force_vehicle, dedicated)?; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl From<u8> for ServerFlags { | ||||||
|  |     fn from(v: u8) -> Self { | ||||||
|  |         ServerFlags { | ||||||
|  |             dedicated: v & 0b1 != 0, | ||||||
|  |             force_vehicle: v & 0b10 != 0, | ||||||
|  |             _rest: (v & 0b11111100) >> 2, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, BinRead)] | ||||||
|  | #[br(little, magic = b"\xba\xce", import(rtt: Duration, addr: SocketAddr))] | ||||||
|  | pub struct Server { | ||||||
|  |     #[br(calc=addr)] | ||||||
|  |     addr: SocketAddr, | ||||||
|  |     #[br(calc=rtt)] | ||||||
|  |     rtt: Duration, | ||||||
|  |     #[br(map = |v: (u8,u8)| format!("{}.{}",v.0,v.1))] | ||||||
|  |     version: String, | ||||||
|  |     port: u16, | ||||||
|  |     max_players: u16, | ||||||
|  |     cur_players: u16, | ||||||
|  |     #[br(map = u8::into)] | ||||||
|  |     flags: ServerFlags, | ||||||
|  |     #[br(pad_size_to(0x20), map = |s :NullString| s.to_string())] | ||||||
|  |     name: String, | ||||||
|  |     #[br(pad_size_to(0x10), map = |s :NullString| s.to_string())] | ||||||
|  |     mode: String, | ||||||
|  |     #[br(pad_size_to(0x20), map = |s :NullString| s.to_string())] | ||||||
|  |     map: String, | ||||||
|  |     _pad: u8, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn pad_copy(d: &[u8], l: usize) -> Vec<u8> { | ||||||
|  |     let diff = d.len() % l; | ||||||
|  |     if diff != 0 { | ||||||
|  |         d.iter() | ||||||
|  |             .copied() | ||||||
|  |             .chain(iter::repeat(0).take(l - diff)) | ||||||
|  |             .collect() | ||||||
|  |     } else { | ||||||
|  |         d.to_vec() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn pad(d: &mut Vec<u8>, l: usize) { | ||||||
|  |     let diff = d.len() % l; | ||||||
|  |     if diff != 0 { | ||||||
|  |         d.extend(iter::repeat(0).take(l - diff)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | struct Packet { | ||||||
|  |     nonce: Vec<u8>, | ||||||
|  |     data: Vec<u8>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Packet { | ||||||
|  |     fn encrypt(data: &[u8]) -> Packet { | ||||||
|  |         let mut data: Vec<u8> = data.to_vec(); | ||||||
|  |         let mut rng = thread_rng(); | ||||||
|  |         let mut nonce = vec![0u8; 12]; | ||||||
|  |         rng.fill(nonce.as_mut_slice()); | ||||||
|  |         let mut cipher = ChaCha20::new(KEY.into(), nonce.as_slice().into()); | ||||||
|  |         cipher.seek(KEY.len() + 32); | ||||||
|  |         cipher.apply_keystream(&mut data); | ||||||
|  |         Packet { nonce, data } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn get_tag(&self) -> Vec<u8> { | ||||||
|  |         let mut sign_data = vec![]; | ||||||
|  |         sign_data.extend(pad_copy(&self.nonce, 16).iter()); | ||||||
|  |         sign_data.extend(pad_copy(&self.data, 16).iter()); | ||||||
|  |         sign_data.extend((self.nonce.len() as u64).to_le_bytes().iter()); | ||||||
|  |         sign_data.extend((self.data.len() as u64).to_le_bytes().iter()); | ||||||
|  |         let mut cipher = ChaCha20::new(KEY.into(), self.nonce.as_slice().into()); | ||||||
|  |         let mut poly_key = *KEY; | ||||||
|  |         cipher.apply_keystream(&mut poly_key); | ||||||
|  |         let signer = Poly1305::new(&poly_key.into()); | ||||||
|  |         signer.compute_unpadded(&sign_data).into_iter().collect() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn bytes(&self) -> Vec<u8> { | ||||||
|  |         let mut data = vec![]; | ||||||
|  |         data.extend(pad_copy(&self.nonce, 16).iter()); | ||||||
|  |         data.extend(pad_copy(&self.data, 16).iter()); | ||||||
|  |         data.extend((self.nonce.len() as u64).to_le_bytes().iter()); | ||||||
|  |         data.extend((self.data.len() as u64).to_le_bytes().iter()); | ||||||
|  |         data.extend(self.get_tag().iter()); | ||||||
|  |         data | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn decrypt(&self) -> Result<Vec<u8>> { | ||||||
|  |         let mut data = self.data.clone(); | ||||||
|  |         let mut sign_data = data.clone(); | ||||||
|  |         pad(&mut sign_data, 16); | ||||||
|  |         let mut nonce = self.nonce.clone(); | ||||||
|  |         pad(&mut nonce, 16); | ||||||
|  |         let sign_data = nonce | ||||||
|  |             .iter() | ||||||
|  |             .chain(sign_data.iter()) | ||||||
|  |             .chain((self.nonce.len() as u64).to_le_bytes().iter()) | ||||||
|  |             .chain((self.data.len() as u64).to_le_bytes().iter()) | ||||||
|  |             .copied() | ||||||
|  |             .collect::<Vec<u8>>(); | ||||||
|  |         let mut poly_key = *KEY; | ||||||
|  |         let mut cipher = ChaCha20::new(KEY.into(), self.nonce.as_slice().into()); | ||||||
|  |         cipher.apply_keystream(&mut poly_key); | ||||||
|  |         let signer = Poly1305::new(&poly_key.into()); | ||||||
|  |         let signature: Vec<u8> = signer.compute_unpadded(&sign_data).into_iter().collect(); | ||||||
|  | 
 | ||||||
|  |         if signature != self.get_tag() { | ||||||
|  |             bail!("Invalid signature!"); | ||||||
|  |         }; | ||||||
|  |         cipher.seek(poly_key.len() + 32); | ||||||
|  |         cipher.apply_keystream(&mut data); | ||||||
|  |         Ok(data) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl TryFrom<&[u8]> for Packet { | ||||||
|  |     type Error = anyhow::Error; | ||||||
|  |     fn try_from(data: &[u8]) -> Result<Self> { | ||||||
|  |         let (mut nonce, data) = data.split_at(16); | ||||||
|  |         let (mut data, tag) = data.split_at(data.len() - 16); | ||||||
|  |         let nonce_len = u64::from_le_bytes(data[data.len() - 16..][..8].try_into()?) as usize; | ||||||
|  |         let data_len = u64::from_le_bytes(data[data.len() - 8..].try_into()?) as usize; | ||||||
|  |         data = &data[..data_len]; | ||||||
|  |         nonce = &nonce[..nonce_len]; | ||||||
|  |         let pkt = Packet { | ||||||
|  |             nonce: nonce.into(), | ||||||
|  |             data: data.into(), | ||||||
|  |         }; | ||||||
|  |         if pkt.get_tag() != tag { | ||||||
|  |             bail!("Invalid signature!"); | ||||||
|  |         } | ||||||
|  |         Ok(pkt) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone)] | ||||||
|  | pub enum ServerEntry { | ||||||
|  |     Alive(Server), | ||||||
|  |     Dead { addr: SocketAddr, reason: String }, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Display for ServerEntry { | ||||||
|  |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
|  |         match self { | ||||||
|  |             ServerEntry::Alive(srv) => write!( | ||||||
|  |                 f, | ||||||
|  |                 "[{}] {} ({} {}/{} Players on {}) version {} [{}] RTT: {:?}", | ||||||
|  |                 srv.addr, | ||||||
|  |                 srv.name, | ||||||
|  |                 srv.mode, | ||||||
|  |                 srv.cur_players, | ||||||
|  |                 srv.max_players, | ||||||
|  |                 srv.map, | ||||||
|  |                 srv.version, | ||||||
|  |                 srv.flags, | ||||||
|  |                 srv.rtt | ||||||
|  |             ), | ||||||
|  |             ServerEntry::Dead { addr, reason } => write!(f, "[{}] (error: {})", addr, reason), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn encrypt(data: &[u8]) -> Vec<u8> { | ||||||
|  |     Packet::encrypt(data).bytes() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn decrypt(data: &[u8]) -> Result<Vec<u8>> { | ||||||
|  |     Packet::try_from(data)?.decrypt() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async fn recv_from_timeout( | ||||||
|  |     sock: &UdpSocket, | ||||||
|  |     buf: &mut [u8], | ||||||
|  |     timeout: f64, | ||||||
|  | ) -> Result<(usize, SocketAddr)> { | ||||||
|  |     Ok(time::timeout(Duration::from_secs_f64(timeout), sock.recv_from(buf)).await??) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async fn query_server<'a>(addr: SocketAddr) -> Result<Server> { | ||||||
|  |     let mut buf = [0; 32 * 1024]; | ||||||
|  |     let socket = UdpSocket::bind("0.0.0.0:0").await?; | ||||||
|  |     socket.connect(addr).await?; | ||||||
|  |     let msg = encrypt(INFO_PACKET); | ||||||
|  |     let t_start = Instant::now(); | ||||||
|  |     socket.send(&msg).await?; | ||||||
|  |     let size = recv_from_timeout(&socket, &mut buf, 5.0).await?.0; | ||||||
|  |     let rtt = t_start.elapsed(); | ||||||
|  |     let data = decrypt(&buf[..size])?; | ||||||
|  |     if !data.starts_with(&[0xba, 0xce]) { | ||||||
|  |         // Server Info
 | ||||||
|  |         bail!("Invalid response"); | ||||||
|  |     } | ||||||
|  |     let mut cur = Cursor::new(&data); | ||||||
|  |     let info: Server = cur.read_le_args((rtt, addr))?; | ||||||
|  |     if info.port != addr.port() { | ||||||
|  |         eprint!("[WARN] Port differs for {}: {}", addr, info.port); | ||||||
|  |     } | ||||||
|  |     if cur.position() != (data.len() as u64) { | ||||||
|  |         bail!("Leftover data"); | ||||||
|  |     } | ||||||
|  |     Ok(info) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async fn get_servers(master_addr: &str) -> Result<(Duration, Vec<ServerEntry>)> { | ||||||
|  |     let master_addr: SocketAddr = master_addr.to_socket_addrs()?.next().unwrap(); | ||||||
|  |     let mut rtt = std::time::Duration::from_secs_f32(0.0); | ||||||
|  |     let mut servers = vec![]; | ||||||
|  |     let mut buf = [0; 32 * 1024]; | ||||||
|  |     let master = UdpSocket::bind("0.0.0.0:0").await?; | ||||||
|  |     master.connect(master_addr).await?; | ||||||
|  |     for n in 0..(256 / 32) { | ||||||
|  |         let data = format!("Brw={},{}\0", n * 32, (n + 1) * 32); | ||||||
|  |         let data = &encrypt(data.as_bytes()); | ||||||
|  |         let t_start = Instant::now(); | ||||||
|  |         master.send(data).await?; | ||||||
|  |         let size = master.recv(&mut buf).await?; | ||||||
|  |         rtt += t_start.elapsed(); | ||||||
|  |         let data = decrypt(&buf[..size])?; | ||||||
|  |         if data.starts_with(b"\0\0\0\0}") { | ||||||
|  |             for chunk in data[5..].chunks(6) { | ||||||
|  |                 if chunk.iter().all(|v| *v == 0) { | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |                 let port = u16::from_le_bytes(chunk[chunk.len() - 2..].try_into()?); | ||||||
|  |                 let addr = SocketAddr::from(([chunk[0], chunk[1], chunk[2], chunk[3]], port)); | ||||||
|  |                 let server = match query_server(addr).await { | ||||||
|  |                     Ok(server) => ServerEntry::Alive(server), | ||||||
|  |                     Err(err) => ServerEntry::Dead { | ||||||
|  |                         addr, | ||||||
|  |                         reason: err.to_string(), | ||||||
|  |                     }, | ||||||
|  |                 }; | ||||||
|  |                 servers.push(server); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     rtt = Duration::from_secs_f64(rtt.as_secs_f64() / ((256 / 32) as f64)); | ||||||
|  |     Ok((rtt, servers)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn indent_hexdump(data: &[u8], indentation: usize, label: &str) -> String { | ||||||
|  |     let mut out = String::new(); | ||||||
|  |     let indent = " ".repeat(indentation); | ||||||
|  |     out.push_str(&indent); | ||||||
|  |     out.push_str(label); | ||||||
|  |     out.push('\n'); | ||||||
|  |     for line in rhexdump::hexdump(data).lines() { | ||||||
|  |         out.push_str(&indent); | ||||||
|  |         out.push_str(line); | ||||||
|  |         out.push('\n'); | ||||||
|  |     } | ||||||
|  |     out.trim_end().to_owned() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Default, Debug)] | ||||||
|  | struct State { | ||||||
|  |     client: BTreeMap<usize, BTreeMap<u8, usize>>, | ||||||
|  |     server: BTreeMap<usize, BTreeMap<u8, usize>>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl State { | ||||||
|  |     fn update_client(&mut self, data: &[u8]) { | ||||||
|  |         data.iter().enumerate().for_each(|(pos, b)| { | ||||||
|  |             *self.client.entry(pos).or_default().entry(*b).or_default() += 1; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     fn update_server(&mut self, data: &[u8]) { | ||||||
|  |         data.iter().enumerate().for_each(|(pos, b)| { | ||||||
|  |             *self.server.entry(pos).or_default().entry(*b).or_default() += 1; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, PartialEq, Eq, Copy, Clone)] | ||||||
|  | enum Direction { | ||||||
|  |     Client, | ||||||
|  |     Server, | ||||||
|  |     Both, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug)] | ||||||
|  | enum CmdResult { | ||||||
|  |     Exit, | ||||||
|  |     Packet { | ||||||
|  |         data: Vec<u8>, | ||||||
|  |         direction: Direction, | ||||||
|  |     }, | ||||||
|  |     Fuzz { | ||||||
|  |         direction: Direction, | ||||||
|  |         start: usize, | ||||||
|  |         end: usize, | ||||||
|  |         chance: (u32, u32), | ||||||
|  |     }, | ||||||
|  |     NoFuzz, | ||||||
|  |     Log(bool), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async fn handle_line( | ||||||
|  |     line: &str, | ||||||
|  |     state: &State, | ||||||
|  |     stdout: &mut SharedWriter, | ||||||
|  | ) -> Result<Option<CmdResult>> { | ||||||
|  |     use CmdResult::*; | ||||||
|  |     let cmd: Vec<&str> = line.trim().split_ascii_whitespace().collect(); | ||||||
|  |     match cmd[..] { | ||||||
|  |         ["log", "off"] => Ok(Some(Log(false))), | ||||||
|  |         ["log", "on"] => Ok(Some(Log(true))), | ||||||
|  |         ["state", pos] => { | ||||||
|  |             let pos = pos.parse()?; | ||||||
|  |             writeln!(stdout, "Client: {:?}", state.client.get(&pos))?; | ||||||
|  |             writeln!(stdout, "Server: {:?}", state.server.get(&pos))?; | ||||||
|  |             Ok(None) | ||||||
|  |         } | ||||||
|  |         [dir @ ("client" | "server"), ref args @ ..] => { | ||||||
|  |             let mut data: Vec<u8> = vec![]; | ||||||
|  |             for args in args.iter() { | ||||||
|  |                 let args = hex::decode(args)?; | ||||||
|  |                 data.extend(args); | ||||||
|  |             } | ||||||
|  |             Ok(Some(CmdResult::Packet { | ||||||
|  |                 data, | ||||||
|  |                 direction: match dir { | ||||||
|  |                     "client" => Direction::Client, | ||||||
|  |                     "server" => Direction::Server, | ||||||
|  |                     _ => unreachable!(), | ||||||
|  |                 }, | ||||||
|  |             })) | ||||||
|  |         } | ||||||
|  |         ["fuzz", dir @ ("client" | "server" | "both"), start, end, chance_num, chance_den] => { | ||||||
|  |             let direction = match dir { | ||||||
|  |                 "client" => Direction::Client, | ||||||
|  |                 "server" => Direction::Server, | ||||||
|  |                 "both" => Direction::Both, | ||||||
|  |                 _ => unreachable!(), | ||||||
|  |             }; | ||||||
|  |             let start = start.parse()?; | ||||||
|  |             let end = end.parse()?; | ||||||
|  |             if start > end { | ||||||
|  |                 bail!("Fuzz start>end"); | ||||||
|  |             } | ||||||
|  |             let res = CmdResult::Fuzz { | ||||||
|  |                 direction, | ||||||
|  |                 start, | ||||||
|  |                 end, | ||||||
|  |                 chance: (chance_num.parse()?, chance_den.parse()?), | ||||||
|  |             }; | ||||||
|  |             Ok(Some(res)) | ||||||
|  |         } | ||||||
|  |         ["fuzz", "off"] => Ok(Some(CmdResult::NoFuzz)), | ||||||
|  |         ["exit"] => Ok(Some(CmdResult::Exit)), | ||||||
|  |         [""] => Ok(None), | ||||||
|  |         _ => bail!("Unknown command: {:?}", line), | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async fn run_proxy( | ||||||
|  |     remote_addr: &SocketAddr, | ||||||
|  |     local_addr: &SocketAddr, | ||||||
|  |     logfile: &Option<PathBuf>, | ||||||
|  | ) -> Result<()> { | ||||||
|  |     let mut print_log = false; | ||||||
|  |     let mut state = State::default(); | ||||||
|  |     let mut logfile = match logfile { | ||||||
|  |         Some(path) => Some(std::fs::File::create(path)?), | ||||||
|  |         None => None, | ||||||
|  |     }; | ||||||
|  |     let mut fuzz = None; | ||||||
|  |     let mut rng = thread_rng(); | ||||||
|  |     let mut client_addr: Option<SocketAddr> = None; | ||||||
|  |     let local = UdpSocket::bind(local_addr).await?; | ||||||
|  |     let remote = UdpSocket::bind("0.0.0.0:0").await?; | ||||||
|  |     remote.connect(remote_addr).await?; | ||||||
|  |     let mut local_buf = vec![0; 32 * 1024]; | ||||||
|  |     let mut remote_buf = vec![0; 32 * 1024]; | ||||||
|  |     println!("Proxy listening on {}", local_addr); | ||||||
|  |     let (mut rl, mut stdout) = Readline::new(format!("{}> ", remote_addr)).unwrap(); | ||||||
|  |     loop { | ||||||
|  |         tokio::select! { | ||||||
|  |             line = rl.readline().fuse() => { | ||||||
|  |                 match line { | ||||||
|  |                     Ok(line) => { | ||||||
|  |                         let line=line.trim(); | ||||||
|  |                         rl.add_history_entry(line.to_owned()); | ||||||
|  |                         match  handle_line(line, &state, &mut stdout).await { | ||||||
|  |                             Ok(Some(result)) => { | ||||||
|  |                                 match result { | ||||||
|  |                                         CmdResult::Packet{data,direction} => { | ||||||
|  |                                             let data=encrypt(&data); | ||||||
|  |                                             match direction { | ||||||
|  |                                                 Direction::Client => { | ||||||
|  |                                                     if client_addr.is_some() { | ||||||
|  |                                                         local | ||||||
|  |                                                             .send_to(&data, client_addr.unwrap()) | ||||||
|  |                                                             .await?; | ||||||
|  |                                                     } else { | ||||||
|  |                                                         writeln!(stdout,"Error: No client address")?; | ||||||
|  |                                                     } | ||||||
|  |                                                 }, | ||||||
|  |                                                 Direction::Server => { | ||||||
|  |                                                     remote.send(&data).await?; | ||||||
|  |                                                 } | ||||||
|  |                                                 Direction::Both => unreachable!() | ||||||
|  |                                             }; | ||||||
|  |                                         } | ||||||
|  |                                         CmdResult::Log(log) => { | ||||||
|  |                                             print_log=log; | ||||||
|  |                                         } | ||||||
|  |                                         CmdResult::Exit => break Ok(()), | ||||||
|  |                                         CmdResult::NoFuzz => { | ||||||
|  |                                             fuzz=None; | ||||||
|  |                                         } | ||||||
|  |                                         CmdResult::Fuzz { .. } => { | ||||||
|  |                                             fuzz=Some(result) | ||||||
|  |                                         }, | ||||||
|  |                                     } | ||||||
|  |                             }, | ||||||
|  |                             Ok(None) => (), | ||||||
|  |                             Err(msg) => { | ||||||
|  |                                 writeln!(stdout, "Error: {}", msg)?; | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |                     Err(ReadlineError::Eof) =>{ writeln!(stdout, "Exiting...")?; break Ok(()) }, | ||||||
|  |                     Err(ReadlineError::Interrupted) => { | ||||||
|  |                         writeln!(stdout, "^C")?; | ||||||
|  |                         break Ok(()); | ||||||
|  |                     }, | ||||||
|  |                     Err(err) => { | ||||||
|  |                         writeln!(stdout, "Received err: {:?}", err)?; | ||||||
|  |                         writeln!(stdout, "Exiting...")?; | ||||||
|  |                         break Ok(()); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             local_res = local.recv_from(&mut local_buf) => { | ||||||
|  |                 let (size, addr) = local_res?; | ||||||
|  |                 client_addr.get_or_insert(addr); | ||||||
|  |                 let mut data = Packet::try_from(&local_buf[..size])?.decrypt()?; | ||||||
|  |                 state.update_client(&data); | ||||||
|  |                 if print_log { | ||||||
|  |                     writeln!(stdout,"{}", indent_hexdump(&data, 0, &format!("OUT: {}", addr)))?; | ||||||
|  |                 } | ||||||
|  |                 if let Some(lf) = logfile.as_mut() { | ||||||
|  |                     writeln!(lf, ">{:?} {} {}", addr, data.len(), hex::encode(&data))?; | ||||||
|  |                 }; | ||||||
|  |                 if let Some(CmdResult::Fuzz{direction,start,end,chance}) = fuzz { | ||||||
|  |                     if (direction==Direction::Server || direction==Direction::Both) && rng.gen_ratio(chance.0,chance.1) { | ||||||
|  |                         rng.fill(&mut data[start..end]); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 remote.send(&encrypt(&data)).await?; | ||||||
|  |             } | ||||||
|  |             remote_res = remote.recv_from(&mut remote_buf) => { | ||||||
|  |                 let (size, addr) = remote_res?; | ||||||
|  |                 let mut data = Packet::try_from(&remote_buf[..size])?.decrypt()?; | ||||||
|  |                 state.update_server(&data); | ||||||
|  |                 if print_log { | ||||||
|  |                     writeln!(stdout,"\r{}", indent_hexdump(&data, 5, &format!("IN: {}", addr)))?; | ||||||
|  |                 } | ||||||
|  |                 if let Some(lf) = logfile.as_mut() { | ||||||
|  |                     writeln!(lf, "<{:?} {} {}", addr, data.len(), hex::encode(&data))?; | ||||||
|  |                 }; | ||||||
|  |                 if client_addr.is_some() { | ||||||
|  |                     if let Some(CmdResult::Fuzz{direction,start,end,chance}) = &fuzz { | ||||||
|  |                         if (*direction==Direction::Client || *direction==Direction::Both) && rng.gen_ratio(chance.0,chance.1) { | ||||||
|  |                             rng.fill(&mut data[*start..*end]); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     local | ||||||
|  |                         .send_to(&encrypt(&data), client_addr.unwrap()) | ||||||
|  |                         .await?; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async fn send_master_cmd(sock: &UdpSocket, cmd: &str) -> Result<Vec<u8>> { | ||||||
|  |     let mut buf = [0; 32 * 1024]; | ||||||
|  |     let mut data: Vec<u8> = cmd.as_bytes().to_vec(); | ||||||
|  |     data.push(0); | ||||||
|  |     let data = &encrypt(&data); | ||||||
|  |     sock.send(data).await?; | ||||||
|  |     let size = recv_from_timeout(sock, &mut buf, 5.0).await?.0; | ||||||
|  |     decrypt(&buf[..size]) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async fn run_master_shell(master_addr: &str) -> Result<()> { | ||||||
|  |     let master = UdpSocket::bind("0.0.0.0:0").await?; | ||||||
|  |     master.connect(master_addr).await?; | ||||||
|  |     let (mut rl, mut stdout) = Readline::new(format!("{}> ", master_addr)).unwrap(); | ||||||
|  |     loop { | ||||||
|  |         tokio::select! { | ||||||
|  |             line = rl.readline().fuse() => { | ||||||
|  |                 match line { | ||||||
|  |                     Ok(line) => { | ||||||
|  |                         let line=line.trim(); | ||||||
|  |                         rl.add_history_entry(line.to_owned()); | ||||||
|  |                         writeln!(stdout,"[CMD] {line}")?; | ||||||
|  |                         match send_master_cmd(&master,line).await { | ||||||
|  |                             Ok(data) => writeln!(stdout,"{}",hexdump(&data))?, | ||||||
|  |                             Err(e) => writeln!(stdout,"Error: {e}")? | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     Err(ReadlineError::Eof) =>{ writeln!(stdout, "Exiting...")?; break Ok(()) }, | ||||||
|  |                     Err(ReadlineError::Interrupted) => { | ||||||
|  |                         writeln!(stdout, "^C")?; | ||||||
|  |                         break Ok(()); | ||||||
|  |                     }, | ||||||
|  |                     Err(err) => { | ||||||
|  |                         writeln!(stdout, "Receive error: {err}")?; | ||||||
|  |                         break Err(err.into()); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Parser, Debug)] | ||||||
|  | #[clap(author, version, about, long_about = None)] | ||||||
|  | struct Args { | ||||||
|  |     /// Server to connect to (if unspecified will query the master server)
 | ||||||
|  |     server: Option<SocketAddr>, | ||||||
|  |     /// Only list servers without starting proxy
 | ||||||
|  |     #[clap(short, long, action)] | ||||||
|  |     list: bool, | ||||||
|  |     /// Local Address to bind to
 | ||||||
|  |     #[clap(short,long, default_value_t = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 28086))] | ||||||
|  |     addr: SocketAddr, | ||||||
|  |     /// Master server to query for running games
 | ||||||
|  |     #[clap(short, long, default_value = "scrapland.mercurysteam.com:5000")] | ||||||
|  |     master: String, | ||||||
|  |     /// Path of file to log decrypted packets to
 | ||||||
|  |     #[clap(short = 'f', long)] | ||||||
|  |     logfile: Option<PathBuf>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[tokio::main] | ||||||
|  | async fn main() -> Result<()> { | ||||||
|  |     let args = Args::parse(); | ||||||
|  |     if args.list && args.server.is_some() { | ||||||
|  |         let addr = args.server.unwrap(); | ||||||
|  |         let server = match query_server(addr).await { | ||||||
|  |             Ok(server) => ServerEntry::Alive(server), | ||||||
|  |             Err(msg) => ServerEntry::Dead { | ||||||
|  |                 addr, | ||||||
|  |                 reason: msg.to_string(), | ||||||
|  |             }, | ||||||
|  |         }; | ||||||
|  |         println!("{}", server); | ||||||
|  |         return Ok(()); | ||||||
|  |     } | ||||||
|  |     if let Some(server) = args.server { | ||||||
|  |         run_proxy(&server, &args.addr, &args.logfile).await?; | ||||||
|  |         return Ok(()); | ||||||
|  |     } | ||||||
|  |     loop { | ||||||
|  |         let (rtt, servers) = get_servers(&args.master).await?; | ||||||
|  |         println!("Master RTT: {:?}", rtt); | ||||||
|  |         if args.list { | ||||||
|  |             for server in servers { | ||||||
|  |                 println!("{}", server); | ||||||
|  |             } | ||||||
|  |             return Ok(()); | ||||||
|  |         } | ||||||
|  |         let selection = Select::with_theme(&ColorfulTheme::default()) | ||||||
|  |             .items(&servers) | ||||||
|  |             .with_prompt("Select server (press Esc to drop into master server command shell)") | ||||||
|  |             .interact_opt()? | ||||||
|  |             .map(|v| &servers[v]); | ||||||
|  |         match selection { | ||||||
|  |             Some(ServerEntry::Dead { addr, reason }) => { | ||||||
|  |                 eprintln!("{:?} returned an error: {}", addr, reason) | ||||||
|  |             } | ||||||
|  |             Some(ServerEntry::Alive(srv)) => { | ||||||
|  |                 return run_proxy(&srv.addr, &args.addr, &args.logfile).await; | ||||||
|  |             } | ||||||
|  |             None => { | ||||||
|  |                 return run_master_shell(&args.master).await; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										151
									
								
								tools/remaster/scrap_net/src/parser.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								tools/remaster/scrap_net/src/parser.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,151 @@ | ||||||
|  | use std::collections::HashMap; | ||||||
|  | use std::error::Error; | ||||||
|  | 
 | ||||||
|  | use crate::hex_ii::hex_ii_dump; | ||||||
|  | use crate::ServerFlags; | ||||||
|  | use binrw::BinReaderExt; | ||||||
|  | use binrw::{binread, BinRead, NullString}; | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | 00000000: 7f 4c 00 00 06 ba ce 01 01 06 63 61 63 6f 74 61 | .L........cacota | ||||||
|  | 00000010: 10 5b 42 4a 5a 5d 20 45 61 72 74 68 6e 75 6b 65 | .[BJZ].Earthnuke | ||||||
|  | 00000020: 72 06 53 50 6f 6c 69 31 37 00 08 50 5f 50 6f 6c | r.SPoli17..P_Pol | ||||||
|  | 00000030: 69 63 65 06 4d 50 4f 4c 49 31 00 00 00 0d 30 2c | ice.MPOLI1....0, | ||||||
|  | 00000040: 30 2c 30 2c 31 2c 30 2c 30 2c 31 00 00 00 00    | 0,0,1,0,0,1.... | ||||||
|  | 
 | ||||||
|  | 00000000: 7f 49 00 00 06 ba ce 01 01 06 63 61 63 6f 74 61 | .I........cacota | ||||||
|  | 00000010: 0e 55 6e 6e 61 6d 65 64 20 50 6c 61 79 65 72 07 | .Unnamed.Player. | ||||||
|  | 00000020: 53 42 65 74 74 79 31 50 00 07 50 5f 42 65 74 74 | SBetty1P..P_Bett | ||||||
|  | 00000030: 79 07 4d 42 65 74 74 79 31 00 00 00 0b 31 2c 31 | y.MBetty1....1,1 | ||||||
|  | 00000040: 2c 30 2c 31 2c 33 2c 30 00 00 00 00             | ,0,1,3,0.... | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone, BinRead)] | ||||||
|  | #[br(big)] | ||||||
|  | #[br(magic = b"\xba\xce")] | ||||||
|  | struct ServerInfoJoin { | ||||||
|  |     #[br(map = |v: (u8,u8)| format!("{}.{}",v.0,v.1))] | ||||||
|  |     version: String, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | struct Data { | ||||||
|  |     player_id: u32, | ||||||
|  |     num_vals: u32, | ||||||
|  |     pos: [f32; 3], | ||||||
|  |     player_index: u32, | ||||||
|  |     rtt: u32, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[binread] | ||||||
|  | #[br(big)] | ||||||
|  | #[derive(Debug, Clone)] | ||||||
|  | enum PacketData { | ||||||
|  |     #[br(magic = b"\x7f")] | ||||||
|  |     PlayerJoin { | ||||||
|  |         data_len: u8, | ||||||
|  |         _1: u8, | ||||||
|  |         cur_players: u8, | ||||||
|  |         max_players: u8, | ||||||
|  |         info: ServerInfoJoin, | ||||||
|  |         #[br(temp)] | ||||||
|  |         pw_len: u8, | ||||||
|  |         #[br(count = pw_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())] | ||||||
|  |         password: String, | ||||||
|  |         #[br(temp)] | ||||||
|  |         player_name_len: u8, | ||||||
|  |         #[br(count = player_name_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())] | ||||||
|  |         player_name: String, | ||||||
|  |         #[br(temp)] | ||||||
|  |         ship_model_len: u8, | ||||||
|  |         #[br(count = ship_model_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())] | ||||||
|  |         ship_model: String, | ||||||
|  |         #[br(little)] | ||||||
|  |         max_health: u16, | ||||||
|  |         #[br(temp)] | ||||||
|  |         pilot_model_len: u8, | ||||||
|  |         #[br(count = pilot_model_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())] | ||||||
|  |         pilot_model: String, | ||||||
|  |         #[br(temp)] | ||||||
|  |         engine_model_r_len: u8, | ||||||
|  |         #[br(count = engine_model_r_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())] | ||||||
|  |         engine_model_r: String, | ||||||
|  |         #[br(temp)] | ||||||
|  |         engine_model_l_len: u8, | ||||||
|  |         #[br(count = engine_model_r_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())] | ||||||
|  |         engine_model_l: String, | ||||||
|  |         _2: u16, | ||||||
|  |         #[br(temp)] | ||||||
|  |         loadout_len: u8, | ||||||
|  |         #[br(count = loadout_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())] | ||||||
|  |         loadout: String, | ||||||
|  |         team_number: u16, | ||||||
|  |         padding: [u8; 2], | ||||||
|  |     }, | ||||||
|  |     #[br(magic = b"\x80\x15")] | ||||||
|  |     MapInfo { | ||||||
|  |         #[br(temp)] | ||||||
|  |         map_len: u32, | ||||||
|  |         #[br(count = map_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())] | ||||||
|  |         map: String, | ||||||
|  |         #[br(temp)] | ||||||
|  |         mode_len: u8, | ||||||
|  |         #[br(count = mode_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())] | ||||||
|  |         mode: String, | ||||||
|  |         _2: u16, | ||||||
|  |         item_count: u8, | ||||||
|  |         // _3: u32,
 | ||||||
|  |         // #[br(count = item_count)]
 | ||||||
|  |         // items: Vec<[u8;0x11]>
 | ||||||
|  |     }, | ||||||
|  |     #[br(magic = b"\xba\xce")] | ||||||
|  |     ServerInfo { | ||||||
|  |         #[br(map = |v: (u8,u8)| format!("{}.{}",v.1,v.0))] | ||||||
|  |         version: String, | ||||||
|  |         port: u16, | ||||||
|  |         max_players: u16, | ||||||
|  |         cur_players: u16, | ||||||
|  |         #[br(map = u8::into)] | ||||||
|  |         flags: ServerFlags, | ||||||
|  |         #[br(pad_size_to(0x20), map=|s: NullString| s.to_string())] | ||||||
|  |         name: String, | ||||||
|  |         #[br(pad_size_to(0x10), map=|s: NullString| s.to_string())] | ||||||
|  |         mode: String, | ||||||
|  |         #[br(pad_size_to(0x20), map=|s: NullString| s.to_string())] | ||||||
|  |         map: String, | ||||||
|  |         _pad: u8, | ||||||
|  |     }, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn parse(data: &[u8]) -> Result<(PacketData, Vec<u8>), Box<dyn Error>> { | ||||||
|  |     use std::io::Cursor; | ||||||
|  |     let mut rdr = Cursor::new(data); | ||||||
|  |     let pkt: PacketData = rdr.read_le()?; | ||||||
|  |     let rest = data[rdr.position() as usize..].to_vec(); | ||||||
|  |     println!("{}", rhexdump::hexdump(data)); | ||||||
|  |     Ok((pkt, rest)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn test_parser() { | ||||||
|  |     let log = include_str!("../test_.log").lines(); | ||||||
|  |     let mut hm = HashMap::new(); | ||||||
|  |     for line in log { | ||||||
|  |         let data = line.split_ascii_whitespace().nth(1).unwrap(); | ||||||
|  |         let data = hex::decode(data).unwrap(); | ||||||
|  |         *hm.entry(data[0..1].to_vec()).or_insert(0usize) += 1; | ||||||
|  |         match parse(&data) { | ||||||
|  |             Ok((pkt, rest)) => { | ||||||
|  |                 println!("{:#x?}", pkt); | ||||||
|  |             } | ||||||
|  |             Err(e) => (), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     let mut hm: Vec<(_, _)> = hm.iter().collect(); | ||||||
|  |     hm.sort_by_key(|(_, v)| *v); | ||||||
|  |     for (k, v) in hm { | ||||||
|  |         let k = k.iter().map(|v| format!("{:02x}", v)).collect::<String>(); | ||||||
|  |         println!("{} {}", k, v); | ||||||
|  |     } | ||||||
|  |     // println!("{:#x?}",parse("8015000000094c6576656c732f465a08466c616748756e7400000100000000000000000000000000000000000004105feb0006003e1125f3bc1300000019007e9dfa0404d5f9003f00000000000000000000"));
 | ||||||
|  |     // println!("{:#x?}",parse("8015000000094c6576656c732f465a08466c616748756e7400002000000000000000000000000000000000000004105feb0006003e1125f3bc1300000019007e9dfa0404d5f9003f000000000000000000001f020b0376a8e2475b6e5b467c1e99461e020903982d14c5ec79cb45b2ee96471d020e03b29dbc46caa433464a28a0c71c020603aa80514658b8ab458db025c71b020803ce492f4658b8ab4514d320c71a02070344532f4658b8ab4587cf16c7190205031b3a0d4658b8ab459eaf25c7180206030ac34c4669e1fd469891ca47170208032e8c2a4669e1fd465500cd4716020703a4952a4669e1fd461b02d247150205037b7c084669e1fd460f92ca4714020603da6b7ec714aa3746b77c5a4713020803c87c83c714aa3746305a5f47120207039a7b83c714aa3746bd5d694711020503bfbe87c714aa3746a67d5a4710020803c5c719474ad5d445a7b3d2c60f0206037c5522474ad5d4459a6edcc60e02070323ca19474ad5d4458dacbec60d020503d84311474ad5d445bb6cdcc60c020603a9b16b47d52d974602dd15470b020803f2236347d52d97467bba1a470a02070350266347d52d974608be24470902050305a05a47d52d9746f1dd1547080206031f4066c6384b9c46955bd345070208037e3b84c6384b9c466147fa4506020703c33684c6384b9c46e431254605020503574395c6384b9c461063d34504020603ba349bc77a60294640f387c103020803957b9fc77a602946658f994402020703677a9fc77a60294680006d45010205038cbda3c77a602946807880c1"));
 | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								tools/remaster/scraphacks_rs/.cargo/config.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								tools/remaster/scraphacks_rs/.cargo/config.toml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | ||||||
|  | [build] | ||||||
|  | target = "i686-pc-windows-msvc" | ||||||
							
								
								
									
										1624
									
								
								tools/remaster/scraphacks_rs/Cargo.lock
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1624
									
								
								tools/remaster/scraphacks_rs/Cargo.lock
									
										
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										45
									
								
								tools/remaster/scraphacks_rs/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								tools/remaster/scraphacks_rs/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | ||||||
|  | [package] | ||||||
|  | name = "scraphacks_rs" | ||||||
|  | version = "0.1.0" | ||||||
|  | edition = "2021" | ||||||
|  | 
 | ||||||
|  | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
|  | [profile.release] | ||||||
|  | debug = 0 | ||||||
|  | 
 | ||||||
|  | [lib] | ||||||
|  | crate-type = ["dylib"] | ||||||
|  | 
 | ||||||
|  | [features] | ||||||
|  | console=[] | ||||||
|  | 
 | ||||||
|  | [dependencies] | ||||||
|  | anyhow = "1.0.68" | ||||||
|  | comma = "1.0.0" | ||||||
|  | custom-print = "0.1.0" | ||||||
|  | derivative = "2.2.0" | ||||||
|  | detour3 = "0.1.0" | ||||||
|  | discord-sdk = "0.3.2" | ||||||
|  | futures = "0.3.25" | ||||||
|  | hex = "0.4.3" | ||||||
|  | iced-x86 = "1.18.0" | ||||||
|  | mlua = { version = "0.8.7", features = ["luajit", "vendored", "macros", "serialize", "mlua_derive"] } | ||||||
|  | nom = "7.1.3" | ||||||
|  | nom-greedyerror = "0.5.0" | ||||||
|  | nom-supreme = "0.8.0" | ||||||
|  | nom_locate = "4.1.0" | ||||||
|  | num-traits = "0.2.15" | ||||||
|  | once_cell = "1.17.0" | ||||||
|  | parse_int = "0.6.0" | ||||||
|  | pelite = "0.10.0" | ||||||
|  | region = "3.0.0" | ||||||
|  | rhexdump = "0.1.1" | ||||||
|  | rustc-hash = "1.1.0" | ||||||
|  | shadow-rs = "0.21.0" | ||||||
|  | struct_layout = "0.1.0" | ||||||
|  | tokio = "1.24.2" | ||||||
|  | viable = "0.2.0" | ||||||
|  | winsafe = { version = "0.0.15", features = ["kernel", "user", "dshow"] } | ||||||
|  | 
 | ||||||
|  | [build-dependencies] | ||||||
|  | shadow-rs = "0.21.0" | ||||||
							
								
								
									
										11
									
								
								tools/remaster/scraphacks_rs/Pipfile
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								tools/remaster/scraphacks_rs/Pipfile
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | [[source]] | ||||||
|  | url = "https://pypi.org/simple" | ||||||
|  | verify_ssl = true | ||||||
|  | name = "pypi" | ||||||
|  | 
 | ||||||
|  | [packages] | ||||||
|  | 
 | ||||||
|  | [dev-packages] | ||||||
|  | 
 | ||||||
|  | [requires] | ||||||
|  | python_version = "3.9" | ||||||
							
								
								
									
										242
									
								
								tools/remaster/scraphacks_rs/Save0.sav.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								tools/remaster/scraphacks_rs/Save0.sav.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,242 @@ | ||||||
|  | { | ||||||
|  |     "id": "Outskirts - 07:41:13", | ||||||
|  |     "title": "Scrapland savegame", | ||||||
|  |     "data": { | ||||||
|  |         "CTFFinishPlayerLoosesTextCad": "Mission_CTFAgainstBankers_RemoteMessage3", | ||||||
|  |         "CTFFinishPlayerWinsTextCad": "Mission_CTFAgainstBankers_RemoteMessage2", | ||||||
|  |         "CTFStartTextCad": "Mission_CTFAgainstBankers_RemoteMessage1", | ||||||
|  |         "CTFOnDeathSpawnTime": "5.0", | ||||||
|  |         "CTFFriendProfile": "BankersCTFFriends", | ||||||
|  |         "CTFEnemyProfile": "BankersCTFEnemies", | ||||||
|  |         "CTFFriendHead": "Functionary", | ||||||
|  |         "CTFFriendType": "Functionary", | ||||||
|  |         "CTFEnemyType": "BankDirector", | ||||||
|  |         "CTFNumFriends": "5", | ||||||
|  |         "CTFNumEnemies": "5", | ||||||
|  |         "CTFFlags": "5", | ||||||
|  |         "CombatFriendProfile": "ArrangeBankersCombatFriends", | ||||||
|  |         "CombatFriendType": "Police", | ||||||
|  |         "CombatNumFriends": "4", | ||||||
|  |         "CombatNumEnemies": "5", | ||||||
|  |         "PlayerWinsCombat": "1", | ||||||
|  |         "OnVictory": "import SaveGame; SaveGame.QWayPoint();import MissionsFuncs; MissionsFuncs.SetNextMission(\"Mission_BackFromMortalRace\", \"GamblinDen\");Scrap.SetSaveVar(\"Map\", \"Levels/GamblinDen\");import SaveGame; SaveGame.QLoad(\"Levels/GamblinDen\");Scrap.SetSaveVar('MapPress_BureauExists','1')", | ||||||
|  |         "OnAbort": "import MissionsFuncs; MissionsFuncs.EndOfSuperDeal('SuperDeal_Faliure_SystemMessage1')", | ||||||
|  |         "CombatFinishPlayerLoosesTextCad": "SuperDeal_First_RemoteMessage3", | ||||||
|  |         "CombatFinishPlayerWinsTextCad": "SuperDeal_First_RemoteMessage2", | ||||||
|  |         "CombatStartTextCad": "SuperDeal_First_RemoteMessage1", | ||||||
|  |         "CombatEnemyProfile": "SuperDealFirstElite", | ||||||
|  |         "CombatEnemyTypeHead": "CrazyGambler", | ||||||
|  |         "CombatEnemyType": "BankDirector", | ||||||
|  |         "CombatDeaths": "5", | ||||||
|  |         "SuperDealType": "", | ||||||
|  |         "IgorFirstContactMissionState": "TalkToMercenaries", | ||||||
|  |         "Stats.MadHunter": "10", | ||||||
|  |         "Stats.Nurse.Dazed": "2", | ||||||
|  |         "Stats.BankMaster": "6758", | ||||||
|  |         "Bank.Circuit.36": "0", | ||||||
|  |         "Bank.Circuit.35": "0", | ||||||
|  |         "Bank.Circuit.34": "0", | ||||||
|  |         "Bank.Circuit.33": "0", | ||||||
|  |         "Bank.Circuit.32": "0", | ||||||
|  |         "Bank.Circuit.31": "0", | ||||||
|  |         "Bank.Circuit.30": "0", | ||||||
|  |         "Bank.Circuit.29": "0", | ||||||
|  |         "Bank.Circuit.28": "0", | ||||||
|  |         "Bank.Circuit.27": "0", | ||||||
|  |         "Bank.Circuit.26": "0", | ||||||
|  |         "Bank.Circuit.25": "0", | ||||||
|  |         "Bank.Circuit.24": "0", | ||||||
|  |         "Bank.Circuit.23": "0", | ||||||
|  |         "Bank.Circuit.22": "0", | ||||||
|  |         "Bank.Circuit.21": "0", | ||||||
|  |         "Bank.Circuit.20": "0", | ||||||
|  |         "Bank.Circuit.19": "0", | ||||||
|  |         "Bank.Circuit.18": "0", | ||||||
|  |         "Bank.Circuit.17": "0", | ||||||
|  |         "Bank.Circuit.16": "0", | ||||||
|  |         "Bank.Circuit.15": "0", | ||||||
|  |         "Bank.Circuit.14": "0", | ||||||
|  |         "Bank.Circuit.13": "0", | ||||||
|  |         "Bank.Circuit.12": "0", | ||||||
|  |         "Bank.Circuit.11": "0", | ||||||
|  |         "Bank.Circuit.10": "0", | ||||||
|  |         "Bank.Circuit.9": "0", | ||||||
|  |         "Bank.Circuit.8": "0", | ||||||
|  |         "Bank.Circuit.7": "0", | ||||||
|  |         "Bank.Circuit.6": "0", | ||||||
|  |         "Bank.Circuit.5": "0", | ||||||
|  |         "Bank.Circuit.4": "0", | ||||||
|  |         "Bank.Circuit.3": "0", | ||||||
|  |         "Bank.Circuit.2": "0", | ||||||
|  |         "Bank.Circuit.1": "0", | ||||||
|  |         "Bank.Circuit.0": "0", | ||||||
|  |         "Stats.Mosquito": "116", | ||||||
|  |         "PoliceBossAtTownHall": "0", | ||||||
|  |         "Stats.Parking": "18", | ||||||
|  |         "Police.FicusDeath": "1", | ||||||
|  |         "CostumeAtPolice": "0", | ||||||
|  |         "Hangar.HangarShip10": "SLifeBoat<-<-<-<-<-0,0,0,0,0,0,1<-30<-0,0,0,0,0,0", | ||||||
|  |         "Hangar.iHangarShips": "8", | ||||||
|  |         "AutoSaveGameOnLoad": "0", | ||||||
|  |         "GameAct": "3rdMurder", | ||||||
|  |         "BankDebt": "0", | ||||||
|  |         "Map": "Levels/Outskirts", | ||||||
|  |         "Mission.Library": "Mission_DestroyBadDebtors", | ||||||
|  |         "EnergyBarActive": "1", | ||||||
|  |         "SpecialActionActive": "2", | ||||||
|  |         "CrazyWing.Status": "1", | ||||||
|  |         "Conversor.ActiveConversors": "1", | ||||||
|  |         "Hangar.iHangarShip": "0", | ||||||
|  |         "Player.NumLives": "100", | ||||||
|  |         "Hangar.shipsToEditList": "['SPoli1', 'SPoli2', 'SPoli3', 'SPoli4', 'SPoli5', 'SPoliBoss1', 'SMerc1', 'SMerc2', 'SMerc3', 'SMayor1', 'SBanker1', 'SBankMaster1', 'SBishop1', 'SArchbishop1', 'SFunc1', 'SBerto1', 'SBetty1', 'SHump1', 'SBoss1', 'SPoli4']", | ||||||
|  |         "Hangar.availableEnginesList": "['MPOLI4', 'MPOLI5', 'MPOLIBOSS1', 'MPOLI2', 'MBERTO1', 'MBETTY1', 'MPOLI1', 'MMERC1', 'MMERC2', 'MPOLI3', 'MMAYOR1', 'MFUNC1', 'MBANKER1', 'MBANKMASTER1', 'MBISHOP1', 'MARCHBISHOP1', 'MHUMP1', 'MBOSS1', 'MMERC3']", | ||||||
|  |         "Hangar.availableWeaponsList": "['Vulcan', 'Devastator', 'Swarm', 'Inferno', 'Tesla', 'ATPC', 'Swarm', 'Devastator']", | ||||||
|  |         "Hangar.availableUpgradesList": "[\"VulcanUpgrade1\", \"VulcanUpgrade2\", \"DevastatorUpgrade1\", \"DevastatorUpgrade2\", \"SwarmUpgrade1\", \"SwarmUpgrade2\", \"InfernoUpgrade1\", \"InfernoUpgrade2\", \"TeslaUpgrade1\", \"TeslaUpgrade2\", \"ATPCUpgrade1\", \"ATPCUpgrade2\"]", | ||||||
|  |         "JackInTheBox.Active": "1", | ||||||
|  |         "Parked.Police": "['SPOLI1', 'SPOLI2', 'SPOLI3', 'SPOLI4']", | ||||||
|  |         "Parked.Mercs": "['SMERC1', 'SMERC2']", | ||||||
|  |         "Parked.TownHall": "['SFUNC1']", | ||||||
|  |         "Parked.Bank": "['SBANKER1']", | ||||||
|  |         "Parked.Press": "['SBERTO1', 'SBETTY1', 'SHUMP1']", | ||||||
|  |         "Parked.Temple": "['SBISHOP1']", | ||||||
|  |         "PoliceBlueprints.Ships": "['SPoli2', 'SPoli3', 'SPoli5', 'SPoliBoss1']", | ||||||
|  |         "PoliceBlueprints.Engines": "['MPoli2', 'MPoli3', 'MPoli4', 'MPoli5', 'MPoliBoss1']", | ||||||
|  |         "PressBlueprints.Ships": "['SBerto1', 'SBetty1', 'SHump1']", | ||||||
|  |         "PressBlueprints.Engines": "['MBerto1', 'MBetty1', 'MHump1']", | ||||||
|  |         "MayorAtGambinDen": "0", | ||||||
|  |         "PolicesAtGambinDen": "1", | ||||||
|  |         "PoliceBossAtPolice": "0", | ||||||
|  |         "CrazyGamblerAtGambinDen": "1", | ||||||
|  |         "FunctionaryTwinsAtGambinDen": "1", | ||||||
|  |         "BankersAtGambinDen": "0", | ||||||
|  |         "BishopsAtGambinDen": "0", | ||||||
|  |         "GameSkill": "0", | ||||||
|  |         "CreateBertos": "0", | ||||||
|  |         "MercsHelpDtritus": "0", | ||||||
|  |         "RobotsControlledByBoss": "0", | ||||||
|  |         "Player.InfiniteLives": "0", | ||||||
|  |         "PrevMap": "Levels/GamblinDen", | ||||||
|  |         "Spawn": "DM_Player_Spawn_GamblinDen", | ||||||
|  |         "Char": "Dtritus", | ||||||
|  |         "ComeFrom": "DoorElevator", | ||||||
|  |         "AlarmActive": "0", | ||||||
|  |         "AlarmStatus": "0.0", | ||||||
|  |         "Money": "2147483647", | ||||||
|  |         "Challenge": "1", | ||||||
|  |         "Challenge.Type": "", | ||||||
|  |         "Challenge.Foe": "", | ||||||
|  |         "Challenge.NumEnemies": "", | ||||||
|  |         "Challenge.Money": "0", | ||||||
|  |         "Revenge.Type": "", | ||||||
|  |         "Revenge.Foe": "", | ||||||
|  |         "Revenge.NumEnemies": "", | ||||||
|  |         "Revenge.LastWinner": "", | ||||||
|  |         "Mission.Map": "Outskirts", | ||||||
|  |         "BadReputation": "3", | ||||||
|  |         "GameplayTime": "27673.5857997", | ||||||
|  |         "LonelyMercInGamblinDen": "0", | ||||||
|  |         "LonelyMercLairActive": "1", | ||||||
|  |         "LonelyMercDataActive": "0", | ||||||
|  |         "ComSatsMissionsMapsFinished": "[]", | ||||||
|  |         "Conversor.AvailableChars": "['Police', 'Nurse', 'BankDirector', 'Desktop', 'Sentinel', 'Gear', 'Bishop', 'Messenger', 'Functionary', 'Betty', 'Berto', 'BankMaster', 'Dtritus']", | ||||||
|  |         "Batteries": "5", | ||||||
|  |         "AcBatteries": "2", | ||||||
|  |         "PrimaryMissionDesc": "Ich muss den Bankdirektor besuchen. Er treibt gerade ausstehende Kreditzahlungen mit seinem Kampfraumschiff ein. Ich sehe mal, ob ich ihm helfen kann.", | ||||||
|  |         "SecondaryMissionDesc": "", | ||||||
|  |         "TakePhotoMsg": "0", | ||||||
|  |         "Race.Num": "3", | ||||||
|  |         "Race.FirstTime": "0", | ||||||
|  |         "Race.Profile": "Pilot", | ||||||
|  |         "Combat.FirstTime": "1", | ||||||
|  |         "Combat.Profile": "Rookie", | ||||||
|  |         "Traffic.AcShips": "[\"SPoli6\", \"SMerc1\",\"SMerc2\",\"SBanker1\"]", | ||||||
|  |         "IsSecondMission": "0", | ||||||
|  |         "CrazyDeal.1.Var": "Stats.Dtritus", | ||||||
|  |         "CrazyDeal.1.Tgt": "5", | ||||||
|  |         "CrazyDeal.2.Var": "Stats.Parking", | ||||||
|  |         "CrazyDeal.2.Tgt": "3", | ||||||
|  |         "CrazyDeal.3.Var": "Stats.Battery", | ||||||
|  |         "CrazyDeal.3.Tgt": "5", | ||||||
|  |         "SuperDeal.Map": "FZ", | ||||||
|  |         "SuperDeal.Library": "SuperDeal_Second", | ||||||
|  |         "SuperDeal.Type": "MortalRace", | ||||||
|  |         "CrazyWing.List": "['Sentinel', 'Betty', 'CrazyGambler', 'Functionary', 'Bishop']", | ||||||
|  |         "Journalist.Humphrey_Defaut": "NoPlace", | ||||||
|  |         "Journalist.Betty_Defaut": "NoPlace", | ||||||
|  |         "Journalist.Berto_Defaut": "Press", | ||||||
|  |         "Conversor.FirstConversion": "0", | ||||||
|  |         "Conversor.FirstPossession": "0", | ||||||
|  |         "WindowsError": "0", | ||||||
|  |         "Orbit.Decontaminated": "yes", | ||||||
|  |         "MercFriends.MercenaryA_Smartie": "0", | ||||||
|  |         "MercFriends.MercenaryC_Brutus": "0", | ||||||
|  |         "MercFriends.MercenaryB_Dumber": "0", | ||||||
|  |         "StdShipAIProfile": "Elite", | ||||||
|  |         "usrShip.Ammo00": "1000.0", | ||||||
|  |         "usrShip.Ammo01": "500.0", | ||||||
|  |         "usrShip.Ammo02": "1500.0", | ||||||
|  |         "usrShip.AcWeap": "6", | ||||||
|  |         "Parking.Desolate": "0", | ||||||
|  |         "Hangar.HangarShip1": "SBoss1<-MBOSS1<-MBOSS1<-<-<-15,15,15,15,15,15,1<-187<-1,8,6,9,11,3", | ||||||
|  |         "Hangar.HangarShip2": "", | ||||||
|  |         "Hangar.HangarShip3": "", | ||||||
|  |         "Hangar.HangarShip4": "", | ||||||
|  |         "Hangar.HangarShip5": "", | ||||||
|  |         "Hangar.HangarShip6": "", | ||||||
|  |         "Hangar.HangarShip7": "", | ||||||
|  |         "Hangar.HangarShip8": "", | ||||||
|  |         "Hangar.HangarShip9": "", | ||||||
|  |         "Hangar.HangarShipAux": "SLifeBoat<-<-<-<-<-0,0,0,0,0,0,1<-50<-0,0,0,0,0,0", | ||||||
|  |         "Hangar.DestroyedShips": "[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]", | ||||||
|  |         "NewBluePrintAvaliable": "11", | ||||||
|  |         "DebugSave": "1", | ||||||
|  |         "OutMusicRelax": "141", | ||||||
|  |         "MayorAtTownHall": "1", | ||||||
|  |         "GamblinMusic": "1", | ||||||
|  |         "OutMusicAction": "122", | ||||||
|  |         "Stats.Traffic": "767", | ||||||
|  |         "Stats.OutPolice": "110", | ||||||
|  |         "MapPress_HiAlarm": "0", | ||||||
|  |         "MapPress_BureauExists": "1", | ||||||
|  |         "Stats.PossessBerto": "1", | ||||||
|  |         "Stats.ConvertIntoDtritus": "1", | ||||||
|  |         "Stats.WinHumphreyRace": "1", | ||||||
|  |         "Stats.Possession": "57", | ||||||
|  |         "Stats.Betty": "49", | ||||||
|  |         "Stats.Killer": "119", | ||||||
|  |         "Stats.Jump": "1", | ||||||
|  |         "Stats.Jump.Police": "1", | ||||||
|  |         "Stats.Bishop": "10", | ||||||
|  |         "Stats.Battery": "0", | ||||||
|  |         "Stats.Dtritus": "0", | ||||||
|  |         "Stats.Race.Press": "1", | ||||||
|  |         "Stats.TotalRaces.Press": "1", | ||||||
|  |         "BankMasterAtBank": "1", | ||||||
|  |         "DM_ExtraLife_00": "-60", | ||||||
|  |         "DM_ExtraLife_01": "-60", | ||||||
|  |         "DM_ExtraLife_02": "-60", | ||||||
|  |         "DM_ExtraLife_03": "-60", | ||||||
|  |         "DM_ExtraLife_04": "16294.7919922", | ||||||
|  |         "Stats.TempleLife": "4", | ||||||
|  |         "Mission_CatchTrurl_Data": "[]", | ||||||
|  |         "Mission_CatchTrurl_MapsPos": "[]", | ||||||
|  |         "Mission_CatchTrurl_NumMapsDropped": "0", | ||||||
|  |         "Mission_CatchTrurl_NumMapsTaken": "0", | ||||||
|  |         "GDB.BishopsMsg": "1", | ||||||
|  |         "Stats.GDB": "6", | ||||||
|  |         "LonelyMercActive": "0", | ||||||
|  |         "Stats.Messenger": "2", | ||||||
|  |         "MortalRaceRace": "[((71611.59375, 18231.6992188, 93232.796875), 422.337219238), ((45388.4140625, 14599.3476563, 79622.6640625), 400.984222412), ((25194.9804688, 18783.4863281, 59759.296875), 404.390136719), ((-433.664245605, 26340.1289063, 34561.0898438), 409.718261719), ((-38229.3671875, 26457.5292969, 679.068054199), 449.472442627), ((-107464.132813, 19331.875, 3288.328125), 528.452758789), ((-113911.117188, 14331.4462891, 40812.9414063), 558.054199219), ((-102532.132813, 11236.1474609, 75072.375), 630.567077637), ((-58177.6289063, 6282.20654297, 74209.3515625), 673.615478516), ((-24157.5449219, 7054.30419922, 43223.1679688), 630.510681152), ((33550.1445313, 15480.2402344, 41122.5820313), -55.4565696716), ((56201.6054688, 15587.5126953, 24649.8496094), 23.7488441467), ((26343.9511719, 22077.8789063, -32317.0292969), 90.5086135864), ((-13835.4755859, 26276.8730469, -31975.1582031), 145.932754517), ((-29244.3652344, 26745.4667969, -2544.81738281), -892.995666504), ((-23808.9570313, 27246.9980469, 32018.1816406), -819.383483887), ((21584.3066406, 29452.4667969, 41221.6171875), -822.313781738), ((54026.796875, 24611.7421875, 42694.0898438), -802.188171387), ((95732.015625, 16516.8085938, 36323.546875), -872.699890137), ((113450.46875, 12325.5195313, 77796.75), -969.003662109)]", | ||||||
|  |         "MortalRaceWaypoints": "20", | ||||||
|  |         "MortalRaceRacers": "['ArchBishop', 'BankDirector', 'Functionary', 'MercenaryA', 'MercenaryB']", | ||||||
|  |         "MortalRaceRacersProfile": "MortalRace", | ||||||
|  |         "MortalRaceFightingFreezeControlAim": "4", | ||||||
|  |         "MortalRaceRespawnTime": "5.0", | ||||||
|  |         "MortalRaceStartTextCad": "Mission_WinMortalRace_RemoteMessage1", | ||||||
|  |         "MortalRaceFinishPlayerLoosesTextCad": "Mission_WinMortalRace_RemoteMessage2", | ||||||
|  |         "MortalRaceFinishPlayerLoosesTextFoe": "Messenger", | ||||||
|  |         "MortalRaceFinishPlayerWinsTextCad": "Mission_WinMortalRace_RemoteMessage3", | ||||||
|  |         "MortalRaceFinishPlayerWinsTextFoe": "Messenger", | ||||||
|  |         "MortalRaceAutoRestart": "1" | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								tools/remaster/scraphacks_rs/build.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								tools/remaster/scraphacks_rs/build.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | fn main() -> shadow_rs::SdResult<()> { | ||||||
|  |     shadow_rs::new() | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								tools/remaster/scraphacks_rs/build.sh
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								tools/remaster/scraphacks_rs/build.sh
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | impo | ||||||
|  | cargo b -r | ||||||
|  | cp D:/devel/Git_Repos/Scrapland-RE/tools/remaster/scraphacks_rs/target/i686-pc-windows-msvc/release/scraphacks_rs.dll E:/Games/Steam/steamapps/common/Scrapland/lib/ScrapHack.pyd | ||||||
|  | # x32dbg E:/Games/Steam/steamapps/common/Scrapland/Bin/Scrap.unpacked.exe "-debug:10 -console" | ||||||
							
								
								
									
										16
									
								
								tools/remaster/scraphacks_rs/notes.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								tools/remaster/scraphacks_rs/notes.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | # Notes | ||||||
|  | 
 | ||||||
|  | ## Snippets | ||||||
|  | 
 | ||||||
|  | Map name: `Scrap.GetLangStr("Station_" + Scrap.GetLevelPath()[7:])` | ||||||
|  | 
 | ||||||
|  | ## Run | ||||||
|  | 
 | ||||||
|  | `steam://run/897610/` | ||||||
|  | 
 | ||||||
|  | ## Signatures | ||||||
|  | 
 | ||||||
|  | - World pointer: `a3 *{'} e8 ? ? ? ? 6a 00 68 *"World initialized"` | ||||||
|  | - print: `6a0068 *{"Scrap engine"} 6a?e8 $'` | ||||||
|  | - console handler: `68 *{"import Viewer"} e8 $'` | ||||||
|  | - DirectX Device: `` | ||||||
							
								
								
									
										34
									
								
								tools/remaster/scraphacks_rs/run.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								tools/remaster/scraphacks_rs/run.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,34 @@ | ||||||
|  | import subprocess as SP | ||||||
|  | import shutil as sh | ||||||
|  | import json | ||||||
|  | from pathlib import Path | ||||||
|  | import psutil | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | os.environ['DISCORD_INSTANCE_ID']='1' | ||||||
|  | SP.check_call(["cargo","b","-r"]) | ||||||
|  | info=[json.loads(line) for line in SP.check_output(["cargo","b", "-r" ,"-q","--message-format=json"]).splitlines()] | ||||||
|  | dll_path=None | ||||||
|  | for line in info: | ||||||
|  |     if line.get('reason')=="compiler-artifact" and ("dylib" in line.get("target",{}).get("crate_types",[])): | ||||||
|  |         dll_path=Path(line['filenames'][0]) | ||||||
|  | 
 | ||||||
|  | sh.copy(dll_path,"E:/Games/Steam/steamapps/common/Scrapland/lib/ScrapHack.pyd") | ||||||
|  | 
 | ||||||
|  | if "--run" not in sys.argv[1:]: | ||||||
|  |     exit(0) | ||||||
|  | 
 | ||||||
|  | os.startfile("steam://run/897610/") | ||||||
|  | pid=None | ||||||
|  | while pid is None: | ||||||
|  |     for proc in psutil.process_iter(): | ||||||
|  |         try: | ||||||
|  |             if proc.name()=="Scrap.exe": | ||||||
|  |                 pid=proc.pid | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
|  | print(f"PID: {pid:x}") | ||||||
|  | if "--dbg" in sys.argv[1:]: | ||||||
|  |     SP.run(["x32dbg","-p",str(pid)]) | ||||||
|  | # cp D:/devel/Git_Repos/Scrapland-RE/tools/remaster/scraphacks_rs/target/i686-pc-windows-msvc/release/scraphacks_rs.dll E:/Games/Steam/steamapps/common/Scrapland/lib/ScrapHack.pyd | ||||||
|  | # x32dbg E:/Games/Steam/steamapps/common/Scrapland/Bin/Scrap.unpacked.exe "-debug:10 -console" | ||||||
							
								
								
									
										16
									
								
								tools/remaster/scraphacks_rs/scrap.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								tools/remaster/scraphacks_rs/scrap.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | #include <stdint.h> | ||||||
|  | struct HashTable { | ||||||
|  |     uint32_t num_slots; | ||||||
|  |     struct HashTableEntry **chains; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | struct HashTableEntry { | ||||||
|  |     void *data; | ||||||
|  |     const char *name; | ||||||
|  |     HashTableEntry *next; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | struct World { | ||||||
|  |     void** VMT; | ||||||
|  |     HashTable *entities; | ||||||
|  | }; | ||||||
							
								
								
									
										7
									
								
								tools/remaster/scraphacks_rs/src/config.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								tools/remaster/scraphacks_rs/src/config.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | enum FilePatch { | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub struct Config { | ||||||
|  |     file_patches: FxHashMap<PathBuf,FilePatch> | ||||||
|  | } | ||||||
							
								
								
									
										94
									
								
								tools/remaster/scraphacks_rs/src/discord.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								tools/remaster/scraphacks_rs/src/discord.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,94 @@ | ||||||
|  | use std::{num::NonZeroU32, thread::JoinHandle, time::SystemTime}; | ||||||
|  | 
 | ||||||
|  | use crate::{cdbg, ceprintln, cprint, cprintln}; | ||||||
|  | use anyhow::{bail, Result}; | ||||||
|  | use discord_sdk::{ | ||||||
|  |     activity::{ActivityBuilder, Assets, PartyPrivacy, Secrets}, | ||||||
|  |     registration::{register_app, Application, LaunchCommand}, | ||||||
|  |     wheel::Wheel, | ||||||
|  |     Discord, DiscordApp, Subscriptions, | ||||||
|  | }; | ||||||
|  | const APP_ID: discord_sdk::AppId = 1066820570097930342; | ||||||
|  | const STEAM_APP_ID: u32 = 897610; | ||||||
|  | pub struct Client { | ||||||
|  |     pub discord: discord_sdk::Discord, | ||||||
|  |     pub user: discord_sdk::user::User, | ||||||
|  |     pub wheel: discord_sdk::wheel::Wheel, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Client { | ||||||
|  |     pub fn run() -> Result<JoinHandle<Result<()>>> { | ||||||
|  |         let rt = tokio::runtime::Builder::new_multi_thread() | ||||||
|  |             .enable_all() | ||||||
|  |             .build()?; | ||||||
|  |         register_app(Application { | ||||||
|  |             id: APP_ID, | ||||||
|  |             name: Some("Scrapland Remastered".to_owned()), | ||||||
|  |             command: LaunchCommand::Steam(STEAM_APP_ID), | ||||||
|  |         })?; | ||||||
|  |         Ok(std::thread::spawn(move || rt.block_on(Self::run_async()))) | ||||||
|  |     } | ||||||
|  |     async fn run_async() -> Result<()> { | ||||||
|  |         let (wheel, handler) = Wheel::new(Box::new(|err| { | ||||||
|  |             ceprintln!("Encountered an error: {}", err); | ||||||
|  |         })); | ||||||
|  |         let mut user = wheel.user(); | ||||||
|  |         let discord = Discord::new( | ||||||
|  |             DiscordApp::PlainId(APP_ID), | ||||||
|  |             Subscriptions::ACTIVITY, | ||||||
|  |             Box::new(handler), | ||||||
|  |         )?; | ||||||
|  |         user.0.changed().await?; | ||||||
|  |         let user = match &*user.0.borrow() { | ||||||
|  |             discord_sdk::wheel::UserState::Connected(user) => user.clone(), | ||||||
|  |             discord_sdk::wheel::UserState::Disconnected(err) => { | ||||||
|  |                 ceprintln!("Failed to connect to Discord: {err}"); | ||||||
|  |                 bail!("{}", err); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |         let uid = user.id; | ||||||
|  |         cprintln!( | ||||||
|  |             "Logged in as: {user}#{discriminator}", | ||||||
|  |             user = user.username, | ||||||
|  |             discriminator = user | ||||||
|  |                 .discriminator | ||||||
|  |                 .map(|d| d.to_string()) | ||||||
|  |                 .unwrap_or_else(|| "????".to_owned()) | ||||||
|  |         ); | ||||||
|  |         let mut activity = ActivityBuilder::new() | ||||||
|  |             .state("Testing") | ||||||
|  |             .assets(Assets::default().large("scrap_logo", Some("Testing"))) | ||||||
|  |             .timestamps(Some(SystemTime::now()), Option::<SystemTime>::None) | ||||||
|  |             .details("Testing ScrapHack"); | ||||||
|  |         if false { | ||||||
|  |             // (SCRAP.is_server()||SCRAP.is_client())
 | ||||||
|  |             let players = 1; | ||||||
|  |             let capacity = 32; | ||||||
|  |             activity = activity | ||||||
|  |                 .instance(true) | ||||||
|  |                 .party( | ||||||
|  |                     "Testt", | ||||||
|  |                     NonZeroU32::new(players), | ||||||
|  |                     NonZeroU32::new(capacity), | ||||||
|  |                     if false { | ||||||
|  |                         PartyPrivacy::Private | ||||||
|  |                     } else { | ||||||
|  |                         PartyPrivacy::Public | ||||||
|  |                     } | ||||||
|  |                 ) | ||||||
|  |                 .secrets(Secrets { | ||||||
|  |                     r#match: Some("MATCH".to_owned()),     // Use server_ip+port
 | ||||||
|  |                     join: Some("JOIN".to_owned()),         // Use server_ip+port
 | ||||||
|  |                     spectate: Some("SPECTATE".to_owned()), // Use server_ip+port
 | ||||||
|  |                 }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         discord.update_activity(activity).await?; | ||||||
|  |         loop { | ||||||
|  |             if let Ok(req) = wheel.activity().0.try_recv() { | ||||||
|  |                 cprintln!("Got Join request: {req:?}"); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										96
									
								
								tools/remaster/scraphacks_rs/src/lib.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								tools/remaster/scraphacks_rs/src/lib.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,96 @@ | ||||||
|  | #![feature(abi_thiscall)] | ||||||
|  | #![feature(c_variadic)] | ||||||
|  | mod discord; | ||||||
|  | mod lua; | ||||||
|  | mod mem; | ||||||
|  | mod parser; | ||||||
|  | mod scrap; | ||||||
|  | use std::ffi::{c_char, c_void, CString}; | ||||||
|  | use anyhow::Result; | ||||||
|  | use crate::mem::search; | ||||||
|  | use crate::scrap::SCRAP; | ||||||
|  | use shadow_rs::shadow; | ||||||
|  | use winsafe::{co::{MB, CS, WS}, prelude::*, HWND, WNDCLASSEX, RegisterClassEx, WString}; | ||||||
|  | 
 | ||||||
|  | shadow!(build); | ||||||
|  | 
 | ||||||
|  | custom_print::define_macros!({cprint, cprintln, cdbg}, fmt, |value: &str| {crate::scrap::SCRAP.print(value)}); | ||||||
|  | custom_print::define_macros!({ceprint, ceprintln}, fmt, |value: &str| {crate::scrap::SCRAP.print_c(0x800000,value)}); | ||||||
|  | 
 | ||||||
|  | #[allow(clippy::single_component_path_imports)] | ||||||
|  | pub(crate) use {cdbg, cprint, cprintln}; | ||||||
|  | #[warn(clippy::single_component_path_imports)] | ||||||
|  | pub(crate) use {ceprint, ceprintln}; | ||||||
|  | 
 | ||||||
|  | #[repr(C)] | ||||||
|  | #[derive(Debug)] | ||||||
|  | struct PyMethodDef { | ||||||
|  |     name: *const c_char, | ||||||
|  |     func: *const (*const c_void, *const c_void), | ||||||
|  |     ml_flags: i32, | ||||||
|  |     doc: *const c_char, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[repr(C)] | ||||||
|  | #[derive(Debug)] | ||||||
|  | struct PyModuleDef { | ||||||
|  |     name: *const c_char, | ||||||
|  |     methods: *const PyMethodDef, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn init_py_mod() { | ||||||
|  |     let py_init_module: fn( | ||||||
|  |         *const c_char,      // name
 | ||||||
|  |         *const PyMethodDef, // methods
 | ||||||
|  |         *const c_char,      // doc
 | ||||||
|  |         *const (),          // passthrough
 | ||||||
|  |         i32,                // module_api_version
 | ||||||
|  |     ) -> *const () = | ||||||
|  |         unsafe { std::mem::transmute(search("68 *{\"Scrap\" 00} e8 ${'}", 1, None).unwrap_or_default()) }; | ||||||
|  |     let name = CString::new("ScrapHack").unwrap_or_default(); | ||||||
|  |     let desc = CString::new("ScrapHack Rust version").unwrap_or_default(); | ||||||
|  |     let methods: &[PyMethodDef] = &[PyMethodDef { | ||||||
|  |         name: 0 as _, | ||||||
|  |         func: 0 as _, | ||||||
|  |         ml_flags: 0, | ||||||
|  |         doc: 0 as _, | ||||||
|  |     }]; | ||||||
|  |     assert!( | ||||||
|  |         !py_init_module(name.as_ptr(), methods.as_ptr(), desc.as_ptr(), 0 as _, 1007).is_null() | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[no_mangle] | ||||||
|  | pub extern "system" fn initScrapHack() { | ||||||
|  |     #[cfg(feature = "console")] | ||||||
|  |     unsafe { | ||||||
|  |         AllocConsole(); | ||||||
|  |     } | ||||||
|  |     std::panic::set_hook(Box::new(|info| { | ||||||
|  |         ceprintln!("ScrapHacks: {info}"); | ||||||
|  |         HWND::DESKTOP | ||||||
|  |             .MessageBox(&format!("{info}"), "ScrapHacks error", MB::ICONERROR) | ||||||
|  |             .unwrap(); | ||||||
|  |         std::process::exit(1); | ||||||
|  |     })); | ||||||
|  |     init_py_mod(); | ||||||
|  |     print_version_info(); | ||||||
|  |     cprintln!("{SCRAP:#x?}"); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[no_mangle] | ||||||
|  | pub extern "system" fn DllMain(_inst: isize, _reason: u32, _: *const u8) -> u32 { | ||||||
|  |     1 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn print_version_info() { | ||||||
|  |     cprintln!( | ||||||
|  |         "{} v{} ({} {}), built for {} by {}.", | ||||||
|  |         build::PROJECT_NAME, | ||||||
|  |         build::PKG_VERSION, | ||||||
|  |         build::SHORT_COMMIT, | ||||||
|  |         build::BUILD_TIME, | ||||||
|  |         build::BUILD_TARGET, | ||||||
|  |         build::RUST_VERSION | ||||||
|  |     ); | ||||||
|  | } | ||||||
							
								
								
									
										204
									
								
								tools/remaster/scraphacks_rs/src/lua.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								tools/remaster/scraphacks_rs/src/lua.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,204 @@ | ||||||
|  | use std::{path::PathBuf, sync::Arc}; | ||||||
|  | 
 | ||||||
|  | use crate::{ | ||||||
|  |     cprintln, | ||||||
|  |     mem::{get_module, get_modules}, | ||||||
|  |     parser::Cmd, | ||||||
|  | }; | ||||||
|  | use anyhow::{anyhow, bail, Result}; | ||||||
|  | use detour3::GenericDetour; | ||||||
|  | use mlua::{prelude::*, Variadic}; | ||||||
|  | use pelite::pattern; | ||||||
|  | use pelite::pe32::{Pe, PeObject}; | ||||||
|  | use rustc_hash::FxHashMap; | ||||||
|  | use winsafe::{prelude::*, HINSTANCE}; | ||||||
|  | 
 | ||||||
|  | struct Ptr(*const ()); | ||||||
|  | 
 | ||||||
|  | impl LuaUserData for Ptr { | ||||||
|  |     fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { | ||||||
|  |         methods.add_meta_method(LuaMetaMethod::ToString, |_, this, _: ()| { | ||||||
|  |             Ok(format!("{:p}", this.0)) | ||||||
|  |         }); | ||||||
|  |         methods.add_method("read", |_, this, (size,): (usize,)| { | ||||||
|  |             let addr = this.0 as u32; | ||||||
|  |             let ptr = this.0 as *const u8; | ||||||
|  |             let info = region::query(ptr).map_err(mlua::Error::external)?; | ||||||
|  |             let end = info.as_ptr_range::<()>().end as u32; | ||||||
|  |             let size = ((end - addr) as usize).min(size); | ||||||
|  |             if !info.is_readable() { | ||||||
|  |                 return Err(LuaError::external(anyhow!("No read permission on page"))); | ||||||
|  |             } | ||||||
|  |             let data = unsafe { std::slice::from_raw_parts(ptr, size) }; | ||||||
|  |             Ok(data.to_vec()) | ||||||
|  |         }); | ||||||
|  |         methods.add_method("write", |_, this, data: Vec<u8>| { | ||||||
|  |             let data = data.as_slice(); | ||||||
|  |             let addr = this.0 as *const u8; | ||||||
|  |             unsafe { | ||||||
|  |                 let handle = region::protect_with_handle( | ||||||
|  |                     addr, | ||||||
|  |                     data.len(), | ||||||
|  |                     region::Protection::READ_WRITE_EXECUTE, | ||||||
|  |                 ) | ||||||
|  |                 .map_err(mlua::Error::external)?; | ||||||
|  |                 std::ptr::copy(data.as_ptr(), addr as *mut u8, data.len()); | ||||||
|  |                 drop(handle); | ||||||
|  |             }; | ||||||
|  |             Ok(()) | ||||||
|  |         }); | ||||||
|  |         // methods.add_method("hook", |_, this, func: LuaFunction| -> LuaResult<()> {
 | ||||||
|  |         //     let addr = this.0;
 | ||||||
|  |         //     cprintln!("Hook: {func:?} @ {addr:p}");
 | ||||||
|  |         //     let dt = unsafe { GenericDetour::<extern "thiscall" fn(*const (), (u32,u32,u32)) -> u32>::new(std::mem::transmute(addr), hook_func) }.unwrap();
 | ||||||
|  |         //     Err(LuaError::external(anyhow!("TODO: hook")))
 | ||||||
|  |         // });
 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // extern "thiscall" fn hook_func(this: *const (), args: (u32,u32,u32)) -> u32 {
 | ||||||
|  | //     return 0;
 | ||||||
|  | // }
 | ||||||
|  | 
 | ||||||
|  | pub(crate) fn init() -> Result<Lua> { | ||||||
|  |     let lua = unsafe { Lua::unsafe_new() }; | ||||||
|  |     { | ||||||
|  |         let globals = lua.globals(); | ||||||
|  |         globals.set("scan", lua.create_function(lua_scan)?)?; | ||||||
|  |         globals.set("print", lua.create_function(lua_print)?)?; | ||||||
|  |         globals.set("hook", lua.create_function(lua_hook)?)?; | ||||||
|  |         globals.set("imports", lua.create_function(lua_imports)?)?; | ||||||
|  |         globals.set( | ||||||
|  |             "ptr", | ||||||
|  |             lua.create_function(|_, addr: u32| Ok(Ptr(addr as _)))?, | ||||||
|  |         )?; | ||||||
|  |         globals.set( | ||||||
|  |             "ptr", | ||||||
|  |             lua.create_function(lua_alloc)?, | ||||||
|  |         )?; | ||||||
|  |     } | ||||||
|  |     Ok(lua) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn lua_val_to_string(val: &LuaValue) -> LuaResult<String> { | ||||||
|  |     Ok(match val { | ||||||
|  |         LuaNil => "Nil".to_owned(), | ||||||
|  |         LuaValue::Boolean(b) => format!("{b}"), | ||||||
|  |         LuaValue::LightUserData(u) => format!("{u:?}"), | ||||||
|  |         LuaValue::Integer(i) => format!("{i}"), | ||||||
|  |         LuaValue::Number(n) => format!("{n}"), | ||||||
|  |         LuaValue::String(s) => (s.to_str()?).to_string(), | ||||||
|  |         LuaValue::Table(t) => { | ||||||
|  |             let mut vals = vec![]; | ||||||
|  |             for res in t.clone().pairs() { | ||||||
|  |                 let (k, v): (LuaValue, LuaValue) = res?; | ||||||
|  |                 vals.push(format!( | ||||||
|  |                     "{k} = {v}", | ||||||
|  |                     k = lua_val_to_string(&k)?, | ||||||
|  |                     v = lua_val_to_string(&v)? | ||||||
|  |                 )); | ||||||
|  |             } | ||||||
|  |             format!("{{{vals}}}", vals = vals.join(", ")) | ||||||
|  |         } | ||||||
|  |         LuaValue::Function(f) => format!("{f:?}"), | ||||||
|  |         LuaValue::Thread(t) => format!("{t:?}"), | ||||||
|  |         LuaValue::UserData(u) => format!("{u:?}"), | ||||||
|  |         LuaValue::Error(e) => format!("{e:?}"), | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn lua_alloc(lua: &Lua, size: usize) -> LuaResult<Ptr> { | ||||||
|  |     let data = vec![0u8;size].into_boxed_slice(); | ||||||
|  |     Ok(Ptr(Box::leak(data).as_ptr() as _)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn lua_hook(lua: &Lua, (addr, func): (u32, LuaFunction)) -> LuaResult<()> { | ||||||
|  |     cprintln!("Hook: {func:?} @ {addr:08x}"); | ||||||
|  |     Err(LuaError::external(anyhow!("TODO: hook"))) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn lua_imports(lua: &Lua, (): ()) -> LuaResult<()> { | ||||||
|  |     Err(LuaError::external(anyhow!("TODO: imports"))) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn lua_print(lua: &Lua, args: Variadic<LuaValue>) -> LuaResult<()> { | ||||||
|  |     let msg: Vec<String> = args | ||||||
|  |         .into_iter() | ||||||
|  |         .map(|v| lua_val_to_string(&v)) | ||||||
|  |         .collect::<LuaResult<Vec<String>>>()?; | ||||||
|  |     cprintln!("{}", msg.join(" ")); | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug)] | ||||||
|  | enum ScanMode { | ||||||
|  |     MainModule, | ||||||
|  |     Modules(Vec<String>), | ||||||
|  |     All, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl FromLua<'_> for ScanMode { | ||||||
|  |     fn from_lua<'lua>(lua_value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> { | ||||||
|  |         match &lua_value { | ||||||
|  |             LuaValue::Nil => Ok(ScanMode::MainModule), | ||||||
|  |             LuaValue::Boolean(true) => Ok(ScanMode::All), | ||||||
|  |             LuaValue::Table(t) => Ok(ScanMode::Modules(FromLua::from_lua(lua_value, lua)?)), | ||||||
|  |             _ => Err(LuaError::FromLuaConversionError { | ||||||
|  |                 from: lua_value.type_name(), | ||||||
|  |                 to: "scan_mode", | ||||||
|  |                 message: None, | ||||||
|  |             }), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn lua_scan(lua: &Lua, (pattern, scan_mode): (String, ScanMode)) -> LuaResult<LuaTable> { | ||||||
|  |     let pat = pattern::parse(&pattern).map_err(mlua::Error::external)?; | ||||||
|  |     let mut ret = FxHashMap::default(); | ||||||
|  |     let modules = match scan_mode { | ||||||
|  |         ScanMode::MainModule => vec![get_module(None).map_err(mlua::Error::external)?], | ||||||
|  |         ScanMode::Modules(modules) => modules | ||||||
|  |             .into_iter() | ||||||
|  |             .map(|m| get_module(Some(m))) | ||||||
|  |             .collect::<Result<_>>() | ||||||
|  |             .map_err(mlua::Error::external)?, | ||||||
|  |         ScanMode::All => get_modules().map_err(mlua::Error::external)?, | ||||||
|  |     }; | ||||||
|  |     'outer: for module in modules { | ||||||
|  |         let regions = region::query_range(module.image().as_ptr(), module.image().len()) | ||||||
|  |             .map_err(mlua::Error::external)?; | ||||||
|  |         for region in regions { | ||||||
|  |             let Ok(region)=region else { | ||||||
|  |                 continue 'outer; | ||||||
|  |             }; | ||||||
|  |             if !region.is_readable() { | ||||||
|  |                 continue 'outer; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         let h_module = unsafe { HINSTANCE::from_ptr(module.image().as_ptr() as _) }; | ||||||
|  |         let module_name = PathBuf::from( | ||||||
|  |             h_module | ||||||
|  |                 .GetModuleFileName() | ||||||
|  |                 .map_err(mlua::Error::external)?, | ||||||
|  |         ) | ||||||
|  |         .file_name() | ||||||
|  |         .unwrap() | ||||||
|  |         .to_str() | ||||||
|  |         .unwrap() | ||||||
|  |         .to_owned(); | ||||||
|  |         if let Ok(res) = crate::mem::scan(&pat, &module) { | ||||||
|  |             if !res.is_empty() { | ||||||
|  |                 let res: Vec<Vec<Ptr>> = res | ||||||
|  |                     .into_iter() | ||||||
|  |                     .map(|res| res.into_iter().map(|a| Ptr(a as _)).collect()) | ||||||
|  |                     .collect(); | ||||||
|  |                 ret.insert(module_name, res); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |     lua.create_table_from(ret.into_iter()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub(crate) fn exec(chunk: &str) -> Result<()> { | ||||||
|  |     Ok(init()?.load(chunk).set_name("ScrapLua")?.exec()?) | ||||||
|  | } | ||||||
							
								
								
									
										94
									
								
								tools/remaster/scraphacks_rs/src/mem.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								tools/remaster/scraphacks_rs/src/mem.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,94 @@ | ||||||
|  | use std::str::FromStr; | ||||||
|  | 
 | ||||||
|  | use anyhow::anyhow; | ||||||
|  | use anyhow::bail; | ||||||
|  | use anyhow::Result; | ||||||
|  | use pelite::pattern::parse; | ||||||
|  | use pelite::pattern::save_len; | ||||||
|  | use pelite::pattern::Atom; | ||||||
|  | use pelite::pe32::{Pe, PeView}; | ||||||
|  | use winsafe::co::TH32CS; | ||||||
|  | use winsafe::prelude::*; | ||||||
|  | use winsafe::HINSTANCE; | ||||||
|  | use winsafe::HPROCESSLIST; | ||||||
|  | pub(crate) struct Pattern(Vec<Atom>, usize); | ||||||
|  | 
 | ||||||
|  | impl Pattern { | ||||||
|  |     pub(crate) fn set_index(mut self, idx: usize) -> Self { | ||||||
|  |         self.1 = idx; | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl FromStr for Pattern { | ||||||
|  |     type Err = anyhow::Error; | ||||||
|  | 
 | ||||||
|  |     fn from_str(s: &str) -> Result<Self, Self::Err> { | ||||||
|  |         Ok(Self(parse(s)?, 0)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Pattern { | ||||||
|  |     pub(crate) fn scan(&self, module: Option<String>) -> Result<u32> { | ||||||
|  |         let pe = get_module(module)?; | ||||||
|  |         let scan = pe.scanner(); | ||||||
|  |         let mut save = vec![0u32; save_len(&self.0)]; | ||||||
|  |         if !scan.finds(&self.0, 0..u32::MAX, &mut save) { | ||||||
|  |             bail!("Pattern not found"); | ||||||
|  |         } | ||||||
|  |         save.get(self.1) | ||||||
|  |             .ok_or_else(|| anyhow!("Result index out of range")) | ||||||
|  |             .and_then(|r| pe.rva_to_va(*r).map_err(|e| e.into())) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub(crate) fn get_modules() -> Result<Vec<PeView<'static>>> { | ||||||
|  |     let mut res = vec![]; | ||||||
|  |     let pid = std::process::id(); | ||||||
|  |     let mut h_snap = HPROCESSLIST::CreateToolhelp32Snapshot(TH32CS::SNAPMODULE, Some(pid))?; | ||||||
|  |     for module in h_snap.iter_modules() { | ||||||
|  |         res.push(unsafe { PeView::module(module?.hModule.as_ptr() as *const u8) }); | ||||||
|  |     } | ||||||
|  |     Ok(res) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub(crate) fn get_module(module: Option<String>) -> Result<PeView<'static>> { | ||||||
|  |     let hmodule = HINSTANCE::GetModuleHandle(module.as_deref())?; | ||||||
|  |     Ok(unsafe { PeView::module(hmodule.as_ptr() as *const u8) }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub(crate) fn scan(pat: &[Atom], pe: &PeView) -> Result<Vec<Vec<u32>>> { | ||||||
|  |     let mut ret = vec![]; | ||||||
|  |     let scan = pe.scanner(); | ||||||
|  |     let mut m = scan.matches(pat, 0..u32::MAX); | ||||||
|  |     let mut save = vec![0u32; save_len(pat)]; | ||||||
|  |     while m.next(&mut save) { | ||||||
|  |         ret.push( | ||||||
|  |             save.iter() | ||||||
|  |                 .map(|rva| pe.rva_to_va(*rva).map_err(|e| e.into())) | ||||||
|  |                 .collect::<Result<Vec<u32>>>()?, | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     Ok(ret) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub(crate) fn search(pat: &str, idx: usize, module: Option<String>) -> Result<u32> { | ||||||
|  |     pat.parse::<Pattern>()?.set_index(idx).scan(module) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn addr_info(addr: u32) -> Result<()> { | ||||||
|  |     let pid = std::process::id(); | ||||||
|  |     let mut h_snap = HPROCESSLIST::CreateToolhelp32Snapshot(TH32CS::SNAPMODULE, Some(pid))?; | ||||||
|  |     for module in h_snap.iter_modules() { | ||||||
|  |         let module = module?; | ||||||
|  |         let module_name = module.szModule(); | ||||||
|  |         if module_name.to_lowercase() == "kernel32.dll" { | ||||||
|  |             continue; | ||||||
|  |         } | ||||||
|  |         let mod_range = | ||||||
|  |             unsafe { module.modBaseAddr..module.modBaseAddr.offset(module.modBaseSize as isize) }; | ||||||
|  |         println!("{module_name}: {mod_range:?}"); | ||||||
|  |         // let module = unsafe { PeView::module(module.modBaseAddr as *const u8) };
 | ||||||
|  |     } | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
							
								
								
									
										177
									
								
								tools/remaster/scraphacks_rs/src/parser.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								tools/remaster/scraphacks_rs/src/parser.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,177 @@ | ||||||
|  | // use crate::{cdbg, ceprintln, cprint, cprintln};
 | ||||||
|  | use std::path::PathBuf; | ||||||
|  | use std::str::FromStr; | ||||||
|  | use anyhow::{anyhow, Result}; | ||||||
|  | use nom::branch::alt; | ||||||
|  | use nom::bytes::complete::{take_till, take_while1}; | ||||||
|  | use nom::character::complete::{digit1, hex_digit1}; | ||||||
|  | use nom::character::streaming::char; | ||||||
|  | use nom::combinator::{eof, opt, rest}; | ||||||
|  | use nom::sequence::{separated_pair, tuple}; | ||||||
|  | use nom::{IResult, Parser}; | ||||||
|  | use nom_locate::LocatedSpan; | ||||||
|  | use nom_supreme::error::ErrorTree; | ||||||
|  | use nom_supreme::final_parser::final_parser; | ||||||
|  | use nom_supreme::tag::complete::{tag, tag_no_case}; | ||||||
|  | use nom_supreme::ParserExt; | ||||||
|  | use pelite::pattern::{self, Atom}; | ||||||
|  | 
 | ||||||
|  | type Span<'a> = LocatedSpan<&'a str>; | ||||||
|  | 
 | ||||||
|  | type ParseResult<'a, 'b, T> = IResult<Span<'a>, T, ErrorTree<Span<'b>>>; | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone)] | ||||||
|  | pub enum Cmd { | ||||||
|  |     Imports, | ||||||
|  |     Read(u32, usize), | ||||||
|  |     ReadPE(u32, usize), | ||||||
|  |     Write(u32, Vec<u8>), | ||||||
|  |     Disams(u32, usize), | ||||||
|  |     Info(Option<u32>), | ||||||
|  |     Script(PathBuf), | ||||||
|  |     Unload, | ||||||
|  |     ScanModule(Vec<Atom>, Option<String>), | ||||||
|  |     Lua(String), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl FromStr for Cmd { | ||||||
|  |     type Err = anyhow::Error; | ||||||
|  | 
 | ||||||
|  |     fn from_str(s: &str) -> Result<Self> { | ||||||
|  |         match parse(s) { | ||||||
|  |             Ok(cmd) => Ok(cmd), | ||||||
|  |             Err(err) => Err(anyhow!("{}", err)), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn ws(input: Span) -> ParseResult<()> { | ||||||
|  |     take_while1(|c: char| c.is_whitespace()) | ||||||
|  |         .value(()) | ||||||
|  |         .context("Whitepace") | ||||||
|  |         .parse(input) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | //
 | ||||||
|  | 
 | ||||||
|  | /// Test
 | ||||||
|  | 
 | ||||||
|  | fn hex_bytes(input: Span) -> ParseResult<Vec<u8>> { | ||||||
|  |     hex_digit1 | ||||||
|  |         .map_res_cut(hex::decode) | ||||||
|  |         .context("Hex string") | ||||||
|  |         .parse(input) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn num(input: Span) -> ParseResult<usize> { | ||||||
|  |     digit1 | ||||||
|  |         .map_res_cut(|n: Span| parse_int::parse(&n)) | ||||||
|  |         .context("Number") | ||||||
|  |         .parse(input) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn address(input: Span) -> ParseResult<u32> { | ||||||
|  |     tag_no_case("0x") | ||||||
|  |         .precedes(hex_digit1) | ||||||
|  |         .recognize() | ||||||
|  |         .map_res_cut(|addr: Span| parse_int::parse::<u32>(&addr)) | ||||||
|  |         .context("Memory address") | ||||||
|  |         .parse(input) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn parse_read_pe(input: Span) -> ParseResult<Cmd> { | ||||||
|  |     tag("read_pe") | ||||||
|  |         .precedes(ws) | ||||||
|  |         .precedes(separated_pair(address, ws, num.opt())) | ||||||
|  |         .map(|(addr, size)| Cmd::ReadPE(addr, size.unwrap_or(0x100))) | ||||||
|  |         .parse(input) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn parse_read(input: Span) -> ParseResult<Cmd> { | ||||||
|  |     tag("read") | ||||||
|  |         .precedes(ws) | ||||||
|  |         .precedes(separated_pair(address, ws, num.opt())) | ||||||
|  |         .map(|(addr, size)| Cmd::Read(addr, size.unwrap_or(0x100))) | ||||||
|  |         .parse(input) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn parse_disasm(input: Span) -> ParseResult<Cmd> { | ||||||
|  |     tag("disasm") | ||||||
|  |         .precedes(ws) | ||||||
|  |         .precedes(separated_pair(address, ws, num.opt())) | ||||||
|  |         .map(|(addr, size)| Cmd::Disams(addr, size.unwrap_or(50))) | ||||||
|  |         .parse(input) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn parse_write(input: Span) -> ParseResult<Cmd> { | ||||||
|  |     tag("write") | ||||||
|  |         .precedes(ws) | ||||||
|  |         .precedes(separated_pair(address, ws, hex_bytes)) | ||||||
|  |         .map(|(addr, data)| Cmd::Write(addr, data)) | ||||||
|  |         .parse(input) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn parse_info(input: Span) -> ParseResult<Cmd> { | ||||||
|  |     tag("info") | ||||||
|  |         .precedes(eof) | ||||||
|  |         .value(Cmd::Info(None)) | ||||||
|  |         .or(tag("info") | ||||||
|  |             .precedes(ws) | ||||||
|  |             .precedes(address) | ||||||
|  |             .map(|addr| Cmd::Info(Some(addr)))) | ||||||
|  |         .parse(input) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn parse_scan(input: Span) -> ParseResult<Cmd> { | ||||||
|  |     let (input, _) = tag("scan").parse(input)?; | ||||||
|  |     let (input, module) = | ||||||
|  |         opt(tuple((char(':'), take_till(|c: char| c.is_whitespace())))).parse(input)?; | ||||||
|  |     let module = module.map(|(_, module)| module.fragment().to_string()); | ||||||
|  |     let (input, _) = ws.parse(input)?; | ||||||
|  |     let (input, pattern) = rest | ||||||
|  |         .map_res(|pat: Span| pattern::parse(&pat)) | ||||||
|  |         .parse(input)?; | ||||||
|  |     Ok((input, Cmd::ScanModule(pattern, module))) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn parse_unload(input: Span) -> ParseResult<Cmd> { | ||||||
|  |     tag("unload").value(Cmd::Unload).parse(input) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn parse_imports(input: Span) -> ParseResult<Cmd> { | ||||||
|  |     tag("imports").value(Cmd::Imports).parse(input) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn parse_lua(input: Span) -> ParseResult<Cmd> { | ||||||
|  |     tag("lua") | ||||||
|  |         .precedes(ws) | ||||||
|  |         .precedes(rest) | ||||||
|  |         .map(|s| Cmd::Lua(s.fragment().to_string())) | ||||||
|  |         .parse(input) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn parse_script(input: Span) -> ParseResult<Cmd> { | ||||||
|  |     tag("script") | ||||||
|  |         .precedes(ws) | ||||||
|  |         .precedes(rest) | ||||||
|  |         .map(|s| Cmd::Script(PathBuf::from(s.fragment()))) | ||||||
|  |         .parse(input) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn parse(input: &str) -> Result<Cmd, ErrorTree<Span<'_>>> { | ||||||
|  |     final_parser( | ||||||
|  |         alt(( | ||||||
|  |             parse_imports, | ||||||
|  |             parse_unload, | ||||||
|  |             parse_scan, | ||||||
|  |             parse_info, | ||||||
|  |             parse_write, | ||||||
|  |             parse_read, | ||||||
|  |             parse_read_pe, | ||||||
|  |             parse_script, | ||||||
|  |             parse_disasm, | ||||||
|  |             parse_lua, | ||||||
|  |         )) | ||||||
|  |         .context("command"), | ||||||
|  |     )(Span::new(input)) | ||||||
|  | } | ||||||
							
								
								
									
										381
									
								
								tools/remaster/scraphacks_rs/src/scrap.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										381
									
								
								tools/remaster/scraphacks_rs/src/scrap.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,381 @@ | ||||||
|  | use crate::{ | ||||||
|  |     cdbg, ceprintln, cprint, cprintln, lua, | ||||||
|  |     mem::{get_module, scan, search}, | ||||||
|  |     parser::Cmd, discord, | ||||||
|  | }; | ||||||
|  | use anyhow::{bail, Result}; | ||||||
|  | use derivative::Derivative; | ||||||
|  | use detour3::GenericDetour; | ||||||
|  | use futures::executor::block_on; | ||||||
|  | use iced_x86::{Decoder, DecoderOptions, Formatter, Instruction, NasmFormatter}; | ||||||
|  | use once_cell::sync::Lazy; | ||||||
|  | use pelite::{pe::PeView, pe32::Pe}; | ||||||
|  | use std::{ | ||||||
|  |     collections::HashMap, | ||||||
|  |     ffi::{c_char, CStr, CString}, | ||||||
|  |     fmt::Debug, | ||||||
|  |     ptr, | ||||||
|  |     thread::JoinHandle, | ||||||
|  |     time::Duration, | ||||||
|  | }; | ||||||
|  | use winsafe::HINSTANCE; | ||||||
|  | use winsafe::{co::TH32CS, prelude::*, HPROCESSLIST}; | ||||||
|  | 
 | ||||||
|  | const POINTER_SIZE: usize = std::mem::size_of::<*const ()>(); | ||||||
|  | 
 | ||||||
|  | #[repr(C)] | ||||||
|  | struct VirtualMethodTable(*const *const ()); | ||||||
|  | 
 | ||||||
|  | impl Debug for VirtualMethodTable { | ||||||
|  |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
|  |         let mut methods = vec![]; | ||||||
|  |         for idx in 0.. { | ||||||
|  |             let ptr = self.get::<()>(idx); | ||||||
|  |             if ptr.is_null() | ||||||
|  |                 || !region::query(ptr) | ||||||
|  |                     .map(|r| r.is_executable()) | ||||||
|  |                     .unwrap_or(false) | ||||||
|  |             { | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             methods.push(ptr); | ||||||
|  |         } | ||||||
|  |         f.debug_tuple("VMT").field(&methods).finish() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl VirtualMethodTable { | ||||||
|  |     fn get<T>(&self, offset: usize) -> *const T { | ||||||
|  |         unsafe { self.0.add(POINTER_SIZE * offset).read() as *const T } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Derivative)] | ||||||
|  | #[derivative(Debug)] | ||||||
|  | pub struct Scrap { | ||||||
|  |     print: extern "C" fn(u32, *const c_char, u8), | ||||||
|  |     console_detour: GenericDetour<extern "C" fn(*const c_char)>, | ||||||
|  |     world: WorldPointer, | ||||||
|  |     discord_thread_handle: JoinHandle<Result<()>>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[repr(C)] | ||||||
|  | #[derive(Debug)] | ||||||
|  | struct Entity { | ||||||
|  |     vmt: VirtualMethodTable, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[repr(C)] | ||||||
|  | #[derive(Debug)] | ||||||
|  | struct HashTableEntry<T> { | ||||||
|  |     data: *const T, | ||||||
|  |     name: *const c_char, | ||||||
|  |     next: *const Self, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[repr(C)] | ||||||
|  | struct HashTable<T> { | ||||||
|  |     num_slots: u32, | ||||||
|  |     chains: *const *const HashTableEntry<T>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn try_read<T>(ptr: *const T) -> Option<T> { | ||||||
|  |     (!ptr.is_null()).then(|| unsafe { ptr.read() }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl<T: std::fmt::Debug> std::fmt::Debug for HashTable<T> { | ||||||
|  |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
|  |         let mut entries: HashMap<String, Option<T>> = HashMap::default(); | ||||||
|  |         for offset in 0..self.num_slots { | ||||||
|  |             let offset = offset as _; | ||||||
|  |             // let chain=vec![];
 | ||||||
|  |             let mut chain_ptr = unsafe { self.chains.offset(offset).read() }; | ||||||
|  |             while !chain_ptr.is_null() { | ||||||
|  |                 let entry = unsafe { chain_ptr.read() }; | ||||||
|  |                 let data = try_read(entry.data); | ||||||
|  |                 let key = unsafe { CStr::from_ptr(entry.name) } | ||||||
|  |                     .to_str() | ||||||
|  |                     .unwrap() | ||||||
|  |                     .to_owned(); | ||||||
|  |                 chain_ptr = entry.next; | ||||||
|  |                 entries.insert(key, data); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         f.debug_struct(&format!("HashTable @ {self:p} ")) | ||||||
|  |             .field("num_slots", &self.num_slots) | ||||||
|  |             .field("entries", &entries) | ||||||
|  |             .finish() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[repr(C)] | ||||||
|  | struct World { | ||||||
|  |     vmt: VirtualMethodTable, | ||||||
|  |     entities: HashTable<Entity>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Debug for World { | ||||||
|  |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
|  |         f.debug_struct("World") | ||||||
|  |             .field("vmt", &self.vmt) | ||||||
|  |             .field("entities", &self.entities) | ||||||
|  |             .finish() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | struct WorldPointer(u32); | ||||||
|  | 
 | ||||||
|  | impl std::fmt::Debug for WorldPointer { | ||||||
|  |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
|  |         let ptr = self.ptr(); | ||||||
|  |         let world = unsafe { ptr.read() }; | ||||||
|  |         f.debug_tuple(&format!("WorldPointer @ {ptr:p} ")) | ||||||
|  |             .field(&world) | ||||||
|  |             .finish() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl WorldPointer { | ||||||
|  |     fn ptr(&self) -> *const World { | ||||||
|  |         let ptr = self.0 as *const *const World; | ||||||
|  |         unsafe { ptr.read() } | ||||||
|  |     } | ||||||
|  |     
 | ||||||
|  |     fn get_hashtable(&self) { | ||||||
|  |         // let ents = unsafe { self.ptr().read().entities.read() };
 | ||||||
|  |         // cprintln!("Ents: {ents:?}");
 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub(crate) static SCRAP: Lazy<Scrap> = | ||||||
|  |     Lazy::new(|| Scrap::init().expect("Failed to initialize Scrap data structure")); | ||||||
|  | 
 | ||||||
|  | impl Scrap { | ||||||
|  |     const PRINT_PATTERN: &str = r#"6a0068 *{"Scrap engine"} 6a?e8 $'"#; | ||||||
|  |     const PY_EXEC: &str = r#"68 *{"import Viewer"} e8 $'"#; | ||||||
|  |     const WORLD_PATTERN: &str = r#"8b 0d *{'} 68 *"CTFFriends""#; | ||||||
|  |     fn init() -> Result<Self> { | ||||||
|  |         let scrap = unsafe { | ||||||
|  |             Self { | ||||||
|  |                 world: WorldPointer(search(Self::WORLD_PATTERN, 1, None)? as _), | ||||||
|  |                 print: std::mem::transmute(search(Self::PRINT_PATTERN, 1, None)?), | ||||||
|  |                 console_detour: GenericDetour::<extern "C" fn(*const c_char)>::new( | ||||||
|  |                     std::mem::transmute(search(Self::PY_EXEC, 1, None)?), | ||||||
|  |                     Self::console_input, | ||||||
|  |                 )?, | ||||||
|  |                 discord_thread_handle: discord::Client::run()?, | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |         unsafe { scrap.console_detour.enable()? } | ||||||
|  |         Ok(scrap) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     extern "C" fn console_input(orig_line: *const c_char) { | ||||||
|  |         let line = unsafe { CStr::from_ptr(orig_line) }.to_str(); | ||||||
|  |         let Ok(line) = line else { | ||||||
|  |             return SCRAP.console_detour.call(orig_line); | ||||||
|  |         }; | ||||||
|  |         if let Some(cmd) = line.strip_prefix('$') { | ||||||
|  |             let res = cmd.parse().and_then(|cmd: Cmd| cmd.exec()); | ||||||
|  |             if let Err(err) = res { | ||||||
|  |                 ceprintln!("Error: {err}"); | ||||||
|  |             } | ||||||
|  |             return; | ||||||
|  |         }; | ||||||
|  |         SCRAP.console_detour.call(orig_line) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn println(&self, msg: &str) { | ||||||
|  |         self.println_c(0x008000, msg) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn print(&self, msg: &str) { | ||||||
|  |         self.print_c(0x008000, msg) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn print_c(&self, col: u32, msg: &str) { | ||||||
|  |         let col = (col & 0xffffff).swap_bytes() >> 8; // 0xRRGGBB -> 0xBBGGRR
 | ||||||
|  |         let msg = CString::new(msg.to_string()).unwrap(); | ||||||
|  |         (self.print)(col, msg.as_ptr(), 0); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn println_c(&self, col: u32, msg: &str) { | ||||||
|  |         let msg = msg.to_owned() + "\n"; | ||||||
|  |         self.print_c(col, &msg) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Cmd { | ||||||
|  |     pub(crate) fn exec(&self) -> Result<()> { | ||||||
|  |         let pe = get_module(None)?; | ||||||
|  |         match self { | ||||||
|  |             Cmd::Imports => { | ||||||
|  |                 for import in pe.imports()? { | ||||||
|  |                     let iat = import.iat()?; | ||||||
|  |                     let int = import.int()?; | ||||||
|  |                     for (func, imp) in iat.zip(int) { | ||||||
|  |                         let imp = imp?; | ||||||
|  |                         cprintln!( | ||||||
|  |                             "{addr:p}: {name} {imp:?}", | ||||||
|  |                             name = import.dll_name()?, | ||||||
|  |                             addr = func | ||||||
|  |                         ); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             Cmd::Read(addr, size) => { | ||||||
|  |                 let ptr = *addr as *const u8; | ||||||
|  |                 let info = region::query(ptr)?; | ||||||
|  |                 let end = info.as_ptr_range::<()>().end as u32; | ||||||
|  |                 let size = ((end - addr) as usize).min(*size); | ||||||
|  |                 if !info.is_readable() { | ||||||
|  |                     bail!("No read permission on page"); | ||||||
|  |                 } | ||||||
|  |                 let data = unsafe { std::slice::from_raw_parts(ptr, size) }; | ||||||
|  |                 cprintln!("{}", &rhexdump::hexdump_offset(data, *addr)); | ||||||
|  |             } | ||||||
|  |             Cmd::Disams(addr, size) => { | ||||||
|  |                 let ptr = *addr as *const u8; | ||||||
|  |                 let info = region::query(ptr)?; | ||||||
|  |                 let end = info.as_ptr_range::<()>().end as u32; | ||||||
|  |                 let size = ((end - addr) as usize).min(*size); | ||||||
|  |                 if !info.is_readable() { | ||||||
|  |                     bail!("No read permission on page"); | ||||||
|  |                 } | ||||||
|  |                 let data = unsafe { std::slice::from_raw_parts(ptr, size) }; | ||||||
|  |                 let mut decoder = Decoder::with_ip(32, data, *addr as u64, DecoderOptions::NONE); | ||||||
|  |                 let mut instruction = Instruction::default(); | ||||||
|  |                 let mut output = String::new(); | ||||||
|  |                 let mut formatter = NasmFormatter::new(); | ||||||
|  |                 while decoder.can_decode() { | ||||||
|  |                     decoder.decode_out(&mut instruction); | ||||||
|  |                     output.clear(); | ||||||
|  |                     formatter.format(&instruction, &mut output); | ||||||
|  |                     cprint!("{:016X} ", instruction.ip()); | ||||||
|  |                     let start_index = (instruction.ip() - (*addr as u64)) as usize; | ||||||
|  |                     let instr_bytes = &data[start_index..start_index + instruction.len()]; | ||||||
|  |                     for b in instr_bytes.iter() { | ||||||
|  |                         cprint!("{:02X}", b); | ||||||
|  |                     } | ||||||
|  |                     cprintln!(" {}", output); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             Cmd::Write(addr, data) => { | ||||||
|  |                 let data = data.as_slice(); | ||||||
|  |                 let addr = *addr as *const u8; | ||||||
|  |                 unsafe { | ||||||
|  |                     let handle = region::protect_with_handle( | ||||||
|  |                         addr, | ||||||
|  |                         data.len(), | ||||||
|  |                         region::Protection::READ_WRITE_EXECUTE, | ||||||
|  |                     )?; | ||||||
|  |                     std::ptr::copy(data.as_ptr(), addr as *mut u8, data.len()); | ||||||
|  |                     drop(handle); | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|  |             Cmd::ReadPE(addr, size) => { | ||||||
|  |                 if !region::query(*addr as *const ())?.is_readable() { | ||||||
|  |                     bail!("No read permission for 0x{addr:x}"); | ||||||
|  |                 } | ||||||
|  |                 let data = pe.read_bytes(*addr)?; | ||||||
|  |                 cprintln!("{}", &rhexdump::hexdump_offset(&data[..*size], *addr)); | ||||||
|  |             } | ||||||
|  |             Cmd::Info(None) => { | ||||||
|  |                 let regions = region::query_range(ptr::null::<()>(), usize::MAX)?; | ||||||
|  |                 for region in regions.flatten() { | ||||||
|  |                     cprintln!( | ||||||
|  |                         "{:?}: {}", | ||||||
|  |                         region.as_ptr_range::<*const ()>(), | ||||||
|  |                         region.protection() | ||||||
|  |                     ); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             Cmd::Info(Some(addr)) => { | ||||||
|  |                 let info = region::query(*addr as *const ())?; | ||||||
|  |                 cprintln!( | ||||||
|  |                     "{:?}: {}", | ||||||
|  |                     info.as_ptr_range::<*const ()>(), | ||||||
|  |                     info.protection() | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  |             Cmd::ScanModule(pat, module) => { | ||||||
|  |                 cprintln!("{:?}", pat); | ||||||
|  |                 let mut total_hits = 0; | ||||||
|  |                 let mut modules = vec![]; | ||||||
|  |                 let is_wildcard = matches!(module.as_deref(), Some("*")); | ||||||
|  |                 if is_wildcard { | ||||||
|  |                     let pid = std::process::id(); | ||||||
|  |                     let mut h_snap = | ||||||
|  |                         HPROCESSLIST::CreateToolhelp32Snapshot(TH32CS::SNAPMODULE, Some(pid))?; | ||||||
|  |                     for module in h_snap.iter_modules() { | ||||||
|  |                         let module = module?; | ||||||
|  |                         let module_name = module.szModule(); | ||||||
|  |                         let module_addr = module.hModule.as_ptr() as *const u8; | ||||||
|  |                         let module = region::query_range(module_addr, module.modBaseSize as usize)? | ||||||
|  |                             .all(|m| m.ok().map(|m| m.is_readable()).unwrap_or(false)) | ||||||
|  |                             .then(|| unsafe { PeView::module(module_addr) }); | ||||||
|  |                         if let Some(module) = module { | ||||||
|  |                             modules.push((module_name, module)); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     let module = HINSTANCE::GetModuleHandle(module.as_deref())?; | ||||||
|  |                     let module_name = module.GetModuleFileName()?; | ||||||
|  |                     let module_addr = module.as_ptr() as *const u8; | ||||||
|  |                     let module = region::query(module_addr) | ||||||
|  |                         .map(|m| m.is_readable()) | ||||||
|  |                         .unwrap_or(false) | ||||||
|  |                         .then(|| unsafe { PeView::module(module_addr) }); | ||||||
|  |                     if let Some(module) = module { | ||||||
|  |                         modules.push((module_name, module)); | ||||||
|  |                     }; | ||||||
|  |                 } | ||||||
|  |                 for (module_name, pe) in modules { | ||||||
|  |                     let res = scan(pat, &pe)?; | ||||||
|  |                     if res.is_empty() { | ||||||
|  |                         continue; | ||||||
|  |                     } | ||||||
|  |                     total_hits += res.len(); | ||||||
|  |                     cprintln!("Module: {module_name}"); | ||||||
|  |                     let sections = pe.section_headers(); | ||||||
|  |                     for hit in &res { | ||||||
|  |                         for (idx, addr) in hit.iter().enumerate() { | ||||||
|  |                             let mut section_name = String::from("<invalid address>"); | ||||||
|  |                             if let Ok(section_rva) = pe.va_to_rva(*addr) { | ||||||
|  |                                 if let Some(section) = sections.by_rva(section_rva) { | ||||||
|  |                                     section_name = match section.name() { | ||||||
|  |                                         Ok(name) => name.to_string(), | ||||||
|  |                                         Err(name_bytes) => format!("{name_bytes:?}"), | ||||||
|  |                                     }; | ||||||
|  |                                 } else { | ||||||
|  |                                     section_name = String::from("<invalid section>"); | ||||||
|  |                                 } | ||||||
|  |                             }; | ||||||
|  |                             if let Ok(region) = region::query(addr) { | ||||||
|  |                                 cprintln!( | ||||||
|  |                                     "\t{}: {:?} {} [{}] {:p}", | ||||||
|  |                                     idx, | ||||||
|  |                                     region.as_ptr_range::<()>(), | ||||||
|  |                                     region.protection(), | ||||||
|  |                                     section_name, | ||||||
|  |                                     addr | ||||||
|  |                                 ) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 cprintln!("Results: {total_hits}"); | ||||||
|  |             } | ||||||
|  |             Cmd::Lua(code) => { | ||||||
|  |                 lua::exec(code)?; | ||||||
|  |             } | ||||||
|  |             Cmd::Script(path) => { | ||||||
|  |                 for line in std::fs::read_to_string(path)?.lines() { | ||||||
|  |                     line.parse().and_then(|cmd: Cmd| cmd.exec())?; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             other => bail!("Not implemented: {other:?}"), | ||||||
|  |         } | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								tools/remaster/scraphacks_rs/target/.rustc_info.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tools/remaster/scraphacks_rs/target/.rustc_info.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | {"rustc_fingerprint":18143952876974389501,"outputs":{"16636649553340150347":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\Users\\Earthnuker\\scoop\\persist\\rustup-msvc\\.rustup\\toolchains\\nightly-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\nfeature=\"cargo-clippy\"\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_has_atomic_equal_alignment=\"16\"\ntarget_has_atomic_equal_alignment=\"32\"\ntarget_has_atomic_equal_alignment=\"64\"\ntarget_has_atomic_equal_alignment=\"8\"\ntarget_has_atomic_equal_alignment=\"ptr\"\ntarget_has_atomic_load_store\ntarget_has_atomic_load_store=\"16\"\ntarget_has_atomic_load_store=\"32\"\ntarget_has_atomic_load_store=\"64\"\ntarget_has_atomic_load_store=\"8\"\ntarget_has_atomic_load_store=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"32\"\ntarget_vendor=\"pc\"\nwindows\n","stderr":""},"4614504638168534921":{"success":true,"status":"","code":0,"stdout":"rustc 1.70.0-nightly (9df3a39fb 2023-04-11)\nbinary: rustc\ncommit-hash: 9df3a39fb30575d808e70800f9fad5362aac57a2\ncommit-date: 2023-04-11\nhost: x86_64-pc-windows-msvc\nrelease: 1.70.0-nightly\nLLVM version: 16.0.2\n","stderr":""},"1185988223601034215":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\Users\\Earthnuker\\scoop\\persist\\rustup-msvc\\.rustup\\toolchains\\nightly-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\nfeature=\"cargo-clippy\"\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_has_atomic_equal_alignment=\"16\"\ntarget_has_atomic_equal_alignment=\"32\"\ntarget_has_atomic_equal_alignment=\"64\"\ntarget_has_atomic_equal_alignment=\"8\"\ntarget_has_atomic_equal_alignment=\"ptr\"\ntarget_has_atomic_load_store\ntarget_has_atomic_load_store=\"16\"\ntarget_has_atomic_load_store=\"32\"\ntarget_has_atomic_load_store=\"64\"\ntarget_has_atomic_load_store=\"8\"\ntarget_has_atomic_load_store=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_thread_local\ntarget_vendor=\"pc\"\nwindows\n","stderr":""}},"successes":{}} | ||||||
							
								
								
									
										3
									
								
								tools/remaster/scraphacks_rs/target/CACHEDIR.TAG
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								tools/remaster/scraphacks_rs/target/CACHEDIR.TAG
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | Signature: 8a477f597d28d172789f06886806bc55 | ||||||
|  | # This file is a cache directory tag created by cargo. | ||||||
|  | # For information about cache directory tags see https://bford.info/cachedir/ | ||||||
|  | @ -0,0 +1,3 @@ | ||||||
|  | Signature: 8a477f597d28d172789f06886806bc55 | ||||||
|  | # This file is a cache directory tag created by cargo. | ||||||
|  | # For information about cache directory tags see https://bford.info/cachedir/ | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue