add Rust ScrapHacks prototype and network sniffer/parser

This commit is contained in:
Daniel S. 2023-05-07 21:36:20 +02:00
parent 58407ecc9f
commit 63962c95cc
27 changed files with 5008 additions and 0 deletions

2
tools/remaster/scrap_net/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
/.history

1015
tools/remaster/scrap_net/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View 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

View 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)

View 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);
}
}

View 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;
}
}
}
}

View 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"));
}

View file

@ -0,0 +1,2 @@
[build]
target = "i686-pc-windows-msvc"

1624
tools/remaster/scraphacks_rs/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View 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"

View file

@ -0,0 +1,11 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
[dev-packages]
[requires]
python_version = "3.9"

View 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"
}
}

View file

@ -0,0 +1,3 @@
fn main() -> shadow_rs::SdResult<()> {
shadow_rs::new()
}

View 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"

View 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: ``

View 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