├── .editorconfig ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE ├── README.md ├── example.png ├── src ├── bin │ ├── tuxtrain.rs │ └── tuxtraind.rs ├── display.rs ├── error.rs ├── hack.rs ├── lib.rs ├── mem.rs ├── pattern.rs ├── trainer.rs └── trainer │ └── daemon.rs ├── trainers ├── darksouls3.toml ├── eldenring.toml ├── example.toml └── sekiro.toml └── tuxtraind.service /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /_bin/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | hard_tabs = true 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tuxtrain" 3 | version = "0.0.4" 4 | authors = ["leaty "] 5 | edition = "2021" 6 | license = "MIT" 7 | readme = "README.md" 8 | description = "An easy-to-use generic trainer for Linux" 9 | repository = "https://github.com/leaty/tuxtrain" 10 | 11 | [[bin]] 12 | name = "tuxtrain" 13 | path = "src/bin/tuxtrain.rs" 14 | 15 | [[bin]] 16 | name = "tuxtraind" 17 | path = "src/bin/tuxtraind.rs" 18 | 19 | [lib] 20 | name = "tuxtrain" 21 | path = "src/lib.rs" 22 | crate-type = ["rlib", "dylib"] 23 | 24 | [dependencies] 25 | clap = { version = "3.1.6", features = ["derive"] } 26 | colored = "2.0.0" 27 | hex = "0.4.3" 28 | nix = "0.23.1" 29 | procfs = "0.12.0" 30 | serde = { version = "1.0.136", features = ["derive"] } 31 | toml = "0.5.8" 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 leaty 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TuxTrain 2 | [![crates.io](https://img.shields.io/crates/v/tuxtrain.svg)](https://crates.io/crates/tuxtrain) 3 | [![Documentation](https://docs.rs/tuxtrain/badge.svg)](https://docs.rs/tuxtrain) 4 | 5 | An easy-to-use generic trainer for Linux written in Rust, with basic memory hacking features. Use it for your games or applications where needed, there is also the `tuxtraind` daemon which will probe for processes and apply matching trainers automatically. 6 | 7 | ![Screenshot](example.png) 8 | 9 | ## Why? 10 | 11 | Well at first I just wanted to run Elden Ring in fullscreen above 60hz, *and* without an FPS lock- I know, weird thing to say in 2022. But then I thought well why not make a generic trainer, can't be too difficult, so here it is. 12 | 13 | ## Trainers 14 | 15 | Trainers in TuxTrain consist of very simple and easily expandable TOML files, see below example. Make sure you also read the [example trainer](https://github.com/leaty/tuxtrain/blob/master/trainers/example.toml) for a breakdown of each setting. 16 | 17 | ```toml 18 | name = "My Trainer" 19 | version = '0.0.1' 20 | process = "someprocess" 21 | enable = true 22 | 23 | [daemon] 24 | delay = 2000 25 | 26 | [[feature]] 27 | name = "FPS Unlock" 28 | region = [7123045, 9124418] 29 | pattern = "FF __ CB 00 7F __ __ D0" 30 | replace = "__ __ 9F 5C __ 58 88 __" 31 | enable = true 32 | 33 | [[feature]] 34 | name = "Infinite Ammo" 35 | region = [52030, 73204] 36 | pattern = "FF 00 __ 00 __ __ 7F 58 D0" 37 | replace = "__ __ __ __ 0B 4C __ __ __" 38 | enable = true 39 | ``` 40 | 41 | ## Installation 42 | 43 | ### Arch Linux 44 | The official AUR packages can be found here: 45 | - [tuxtrain](https://aur.archlinux.org/packages/tuxtrain) 46 | - [tuxtrain-git](https://aur.archlinux.org/packages/tuxtrain-git) 47 | 48 | *Or* use an AUR helper like [paru](https://github.com/Morganamilo/paru) or [yay](https://github.com/Jguer/yay). 49 | ```bash 50 | paru -S tuxtrain 51 | ``` 52 | 53 | ### Manually 54 | Clone and run: 55 | ``` 56 | cargo install --path . 57 | ``` 58 | 59 | This will by default install `tuxtrain` and `tuxtraind` to `~/.cargo/bin`. You can add this to your PATH if you wish. 60 | 61 | Next, create the directory `/etc/tuxtrain` and copy the trainers you want there. Otherwise you can only use the `tuxtrain -t path/to/trainer.toml` method. 62 | 63 | ## Running TuxTrain 64 | Since accessing and writing memory in foreign processes require certain permissions, TuxTrain must almost always run as root, unless you manually take ownership of the process. 65 | 66 | This will run all trainers from `/etc/tuxtrain/*` *once*. Naturally, nothing will happen if the programs the trainers look for aren't running. Also, you can run a single trainer from `/etc/tuxtrain/*` or even specify a file, see `--help` for other options. 67 | ``` 68 | sudo tuxtrain 69 | ``` 70 | 71 | ## Running TuxTrain daemon 72 | This will probe all trainers from `/etc/tuxtrain/*` every second, which is also the default rate. Once a program is discovered, the trainers matching it will execute. The same trainer will *not* run again while the program is still running, but once it is stopped and started again the same trainer will trigger. 73 | ``` 74 | sudo tuxtraind --rate 1000 75 | ``` 76 | 77 | ## Running as a service 78 | This is probably what most people want, starting it automatically at boot and whenever you launch your favorite game or program, the matching trainer(s) are automatically applied. Even if you don't want something like "Infinite Ammo" all the time, you should instead disable this in the trainer, because having an FPS unlocker apply automatically is quite nice. 79 | 80 | There is an example service [here](https://github.com/leaty/tuxtrain/blob/master/tuxtraind.service). 81 | 82 | ## Contribute 83 | 84 | ### Trainers 85 | I urge anyone with useful hacks or features to contribute with trainers in order to have a good set of official [trainers](https://github.com/leaty/tuxtrain/tree/master/trainers) in this repository making sharing and redistribution a cakewalk. It could be almost anything; application automation, FPS unlockers, mouse acceleration patches, or even infinite ammo or infinite health etc. However, make no mistake- I completely abhor cheating, but as long as it's for singleplayer experiences it's perfectly OK. 86 | 87 | ### Code 88 | There may be important features missing for certain trainer operations, I am in no way an expert on trainers so if there's something important in which the current search/replace functionality cannot cover, please reach out! 89 | 90 | ## DISCLAIMER 91 | Memory hacking is fragile, I bear no responsibility if something either doesn't work or if something breaks. Thankfully memory is also temporary, so in most cases a simple restart of the program or a reboot will fix it. 92 | 93 | ### Anti-Cheat 94 | TuxTrain is always going to be intended for offline use, so this is never about cheating. But, if you intend to use the Elden Ring trainer, **I ADVICE YOU TO NOT RUN THIS WITH EAC ENABLED**. The same goes for any future trainer in this repository, whether it be EAC or BattleEye or something else. 95 | 96 | I bear no responsibility whatsoever for any potential bans, remember this is memory hacking after all and it could be seen as nefarious even when it's not. In fact in this particular case, it kind of *is* nefarious (thanks to FromSoftware) since other players in Elden Ring are limited to 60 FPS. 97 | 98 | TuxTrain is generic and it doesn't care what you are doing, therefor it does **NOT** care about something like EAC. I've not yet heard anyone neither try nor getting banned for unlocking their FPS, but I don't think you'd want to be the first either. 99 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaty/tuxtrain/a9207bb2e52c11a6c517719c4f642a62c846e284/example.png -------------------------------------------------------------------------------- /src/bin/tuxtrain.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::error::Error; 3 | use std::path::Path; 4 | use std::path::PathBuf; 5 | use tuxtrain::trainer; 6 | 7 | const TRAINER_DIR: &str = "/etc/tuxtrain"; 8 | 9 | #[derive(Debug, Parser)] 10 | #[clap(author, version, about, long_about = None)] 11 | pub struct Args { 12 | #[clap(help = r#"Trainer to run, based on /etc/tuxtrain/.toml 13 | Omit this field to run all trainers"#)] 14 | trainer: Option, 15 | #[clap(short, long, help = "Load trainer directly from file")] 16 | trainer_file: Option, 17 | #[clap(short, long, help = "Don't print messages")] 18 | silent: bool, 19 | #[clap(short, long, help = "Full memory scan, ignoring region presets")] 20 | full: bool, 21 | } 22 | 23 | fn main() -> Result<(), Box> { 24 | let dir = Path::new(TRAINER_DIR); 25 | let args = Args::parse(); 26 | let trainers = trainer::from_args(dir, args.trainer, args.trainer_file)?; 27 | 28 | // Run trainers 29 | for trainer in trainers { 30 | if trainer.enable { 31 | trainer.run(args.full, !args.silent); 32 | } 33 | } 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /src/bin/tuxtraind.rs: -------------------------------------------------------------------------------- 1 | //! TODO Remove WETness, since this is mostly a modified copy from tuxtrain bin. 2 | use clap::Parser; 3 | use std::collections::HashSet; 4 | use std::error::Error; 5 | use std::path::Path; 6 | use std::path::PathBuf; 7 | use std::thread; 8 | use std::time::Duration; 9 | use tuxtrain::{trainer, Trainer}; 10 | 11 | const TRAINER_DIR: &str = "/etc/tuxtrain"; 12 | 13 | #[derive(Debug, Parser)] 14 | #[clap(author, version, about, long_about = None)] 15 | pub struct Args { 16 | #[clap(help = r#"Trainer to run, based on /etc/tuxtrain/.toml 17 | Omit this field to run all trainers"#)] 18 | trainer: Option, 19 | #[clap(short, long, help = "Load trainer directly from file")] 20 | trainer_file: Option, 21 | #[clap(short, long, help = "Don't print messages")] 22 | silent: bool, 23 | #[clap(short, long, help = "Full memory scan, ignoring region presets")] 24 | full: bool, 25 | #[clap( 26 | short, 27 | long, 28 | help = "Rate (ms) to probe processes", 29 | default_value_t = 1000 30 | )] 31 | rate: u64, 32 | } 33 | 34 | fn main() -> Result<(), Box> { 35 | let dir = Path::new(TRAINER_DIR); 36 | let args = Args::parse(); 37 | let rate = Duration::from_millis(args.rate); 38 | let trainers = trainer::from_args(dir, args.trainer, args.trainer_file)?; 39 | let mut status = HashSet::new(); 40 | 41 | loop { 42 | for (idx, trainer) in trainers.iter().enumerate() { 43 | if trainer.enable { 44 | // When process is found and it hasn't 45 | // already been run for this instance 46 | if trainer.probe().is_ok() { 47 | if !status.contains(&idx) { 48 | delay(trainer); 49 | trainer.run(args.full, !args.silent); 50 | status.insert(idx); 51 | } 52 | // Process disappeared 53 | } else { 54 | status.remove(&idx); 55 | } 56 | } 57 | } 58 | 59 | thread::sleep(rate); 60 | } 61 | } 62 | 63 | /// Waits according to trainer.daemon.delay if any 64 | fn delay(trainer: &Trainer) { 65 | if let Some(daemon) = &trainer.daemon { 66 | if let Some(delay) = daemon.delay { 67 | thread::sleep(Duration::from_millis(delay)); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/display.rs: -------------------------------------------------------------------------------- 1 | //! Improve this code someday. 2 | //! But honestly, printing is always ugly. 3 | 4 | use crate::{error::TrainerError, Hack, HackInfo, HackResult, Trainer}; 5 | use colored::*; 6 | 7 | pub fn trainer(trainer: &Trainer) { 8 | println!( 9 | "{} {} {}", 10 | "==>".magenta().bold(), 11 | trainer.name.magenta().bold(), 12 | trainer.version.blue().bold() 13 | ); 14 | } 15 | 16 | pub fn trainer_err(err: &TrainerError) { 17 | println!("\t{}: {}", "Notice".red().bold(), err); 18 | } 19 | 20 | pub fn feature(hack: &Hack) { 21 | println!("\t{}", hack.name.green().bold()); 22 | } 23 | 24 | pub fn feature_result(hack: &Hack, result: &HackResult, full: bool) { 25 | match result { 26 | Ok(info) => { 27 | println!("\t - {}: {}", "Location".cyan(), info.at); 28 | println!( 29 | "\t - {}: {}", 30 | "Found".cyan(), 31 | hex::encode(&info.found).to_uppercase() 32 | ); 33 | println!( 34 | "\t - {}: {}", 35 | "Wrote".cyan(), 36 | hex::encode(&info.wrote).to_uppercase() 37 | ); 38 | 39 | feature_consider(hack, info, full); 40 | } 41 | Err(e) => { 42 | println!("\t - {}: {}", "Error".red().bold(), e) 43 | } 44 | } 45 | } 46 | 47 | pub fn feature_consider(hack: &Hack, info: &HackInfo, full: bool) { 48 | let mut consider = true; 49 | if let (false, Some(region)) = (full, hack.region) { 50 | // Region is already as tight as it gets 51 | if info.at == region.0 && info.at + info.found.len() == region.1 { 52 | consider = false; 53 | } 54 | }; 55 | 56 | if consider { 57 | println!( 58 | "\t - {}: [{}, {}]", 59 | "Consider tightening region".cyan(), 60 | info.at, 61 | info.at + info.found.len(), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use procfs::ProcError; 2 | use std::fmt; 3 | 4 | pub enum TrainerError { 5 | Process(String), 6 | } 7 | 8 | pub enum HackError { 9 | Read(String), 10 | Write(String), 11 | } 12 | 13 | pub enum MemError { 14 | Read(String), 15 | Write(String), 16 | } 17 | 18 | impl fmt::Display for TrainerError { 19 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 20 | match self { 21 | TrainerError::Process(e) => write!(f, "{}", e), 22 | } 23 | } 24 | } 25 | 26 | impl fmt::Display for HackError { 27 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 28 | match self { 29 | HackError::Read(e) => write!(f, "{}", e), 30 | HackError::Write(e) => write!(f, "{}", e), 31 | } 32 | } 33 | } 34 | 35 | impl From for TrainerError { 36 | fn from(err: ProcError) -> Self { 37 | TrainerError::Process(err.to_string()) 38 | } 39 | } 40 | 41 | impl From for HackError { 42 | fn from(err: ProcError) -> Self { 43 | HackError::Read(err.to_string()) 44 | } 45 | } 46 | 47 | impl From for HackError { 48 | fn from(err: MemError) -> Self { 49 | match err { 50 | MemError::Read(e) => HackError::Read(e), 51 | MemError::Write(e) => HackError::Write(e), 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/hack.rs: -------------------------------------------------------------------------------- 1 | use crate::error::HackError; 2 | use crate::mem; 3 | use crate::{pattern, Pattern}; 4 | use nix::unistd::Pid; 5 | use procfs::process::Process; 6 | use serde::Deserialize; 7 | 8 | #[derive(Deserialize)] 9 | pub struct Hack { 10 | pub name: String, 11 | pub enable: bool, 12 | #[serde(deserialize_with = "pattern::deserialize")] 13 | pub pattern: Pattern, 14 | #[serde(deserialize_with = "pattern::deserialize")] 15 | pub replace: Pattern, 16 | pub region: Option<(usize, usize)>, 17 | } 18 | 19 | impl Hack { 20 | pub fn run(&self, proc: &Process, full: bool) -> HackResult { 21 | let pid = Pid::from_raw(proc.pid); 22 | 23 | // Set which region to scan, or full memory scan 24 | let regions = match (full, self.region.clone()) { 25 | (false, Some(r)) => vec![r], 26 | _ => proc 27 | .maps()? 28 | .iter() 29 | .map(|m| (m.address.0 as usize, m.address.1 as usize)) 30 | .collect(), 31 | }; 32 | 33 | for region in regions { 34 | if let Ok((at, found)) = mem::search(&pid, ®ion, &self.pattern) { 35 | let wrote = mem::replace(&pid, at, &found, &self.replace)?; 36 | return Ok(HackInfo { at, found, wrote }); 37 | } 38 | } 39 | 40 | Err(HackError::Read("Unable to find pattern.".into())) 41 | } 42 | } 43 | 44 | pub struct HackInfo { 45 | pub at: usize, 46 | pub found: Vec, 47 | pub wrote: Vec, 48 | } 49 | 50 | pub type HackResult = Result; 51 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod display; 2 | pub mod error; 3 | pub mod hack; 4 | pub mod mem; 5 | pub mod pattern; 6 | pub mod trainer; 7 | 8 | pub use hack::{Hack, HackInfo, HackResult}; 9 | pub use pattern::Pattern; 10 | pub use trainer::Trainer; 11 | -------------------------------------------------------------------------------- /src/mem.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MemError; 2 | use crate::Pattern; 3 | use nix::errno::Errno; 4 | use nix::sys::uio::{process_vm_readv, process_vm_writev}; 5 | use nix::sys::uio::{IoVec, RemoteIoVec}; 6 | use nix::unistd::Pid; 7 | use std::fs::File; 8 | use std::io; 9 | use std::io::{Seek, SeekFrom, Write}; 10 | 11 | const CHUNK_SIZE: usize = 1048576; 12 | 13 | pub fn search( 14 | pid: &Pid, 15 | region: &(usize, usize), 16 | pattern: &Pattern, 17 | ) -> Result<(usize, Vec), MemError> { 18 | let end = region.1; 19 | let find = pattern.len(); 20 | let mut chunk_size = CHUNK_SIZE; 21 | let mut pointer = region.0; 22 | let mut found = vec![]; 23 | let mut at = 0; 24 | 25 | loop { 26 | // Avoid overreach 27 | if pointer + chunk_size > end { 28 | chunk_size = end - pointer; 29 | } 30 | 31 | // Read memory region one chunk at a time 32 | let chunk = read(pid, pointer, chunk_size)?; 33 | 34 | // Go through chunk per byte, forward-match with pattern each time 35 | for i in 0..chunk.len() { 36 | chunk 37 | .iter() 38 | .skip(i) 39 | .zip(pattern.iter().skip(found.len())) 40 | .all(|(mbyte, cbyte)| { 41 | match cbyte { 42 | // Store matching bytes 43 | // None "__" criteria always matches 44 | Some(b) if b == mbyte => found.push(*mbyte), 45 | None => found.push(*mbyte), 46 | 47 | // Doesn't match, reset 48 | _ => { 49 | found.clear(); 50 | return false; 51 | } 52 | }; 53 | 54 | // Set "at" on first discovery 55 | if found.len() == 1 { 56 | at = pointer + i; 57 | } 58 | 59 | return true; 60 | }); 61 | 62 | // Found what there is to find 63 | if found.len() == find { 64 | return Ok((at, found)); 65 | } 66 | } 67 | 68 | // Set next chunk 69 | pointer += chunk.len(); 70 | 71 | // End of region, never found it sadly 72 | if pointer == end { 73 | break; 74 | } 75 | } 76 | 77 | Err(MemError::Read("Could not find pattern '{pattern}'".into()))? 78 | } 79 | 80 | pub fn replace(pid: &Pid, at: usize, this: &Vec, with: &Pattern) -> Result, MemError> { 81 | if this.len() != with.len() { 82 | Err(MemError::Write("Replacement differs in length.".into()))? 83 | } 84 | 85 | // Merge "this" "with" 86 | // None: Use left 87 | // Some: Use right 88 | let mut replace = vec![]; 89 | for (i, b) in this.iter().enumerate() { 90 | replace.push(with[i].unwrap_or_else(|| *b)); 91 | } 92 | 93 | write(pid, at, &replace)?; 94 | 95 | Ok(replace) 96 | } 97 | 98 | pub fn read(pid: &Pid, at: usize, len: usize) -> Result, MemError> { 99 | let mut data = vec![0; len]; 100 | let local = IoVec::<&mut [u8]>::from_mut_slice(&mut data); 101 | let remote = RemoteIoVec { base: at, len }; 102 | 103 | process_vm_readv(*pid, &[local], &[remote]).map_err(|e| MemError::Read(e.to_string()))?; 104 | Ok(data) 105 | } 106 | 107 | pub fn write(pid: &Pid, at: usize, data: &Vec) -> Result { 108 | let local = IoVec::<&[u8]>::from_slice(data); 109 | let remote = RemoteIoVec { 110 | base: at, 111 | len: data.len(), 112 | }; 113 | 114 | // Try to write directly. On EFAULT, it's likely a 115 | // protected page so try ptrace instead as fallback 116 | match process_vm_writev(*pid, &[local], &[remote]) { 117 | Err(Errno::EFAULT) => { 118 | write_protected(pid, at as u64, data).map_err(|e| MemError::Write(e.to_string())) 119 | } 120 | r => r.map_err(|e| MemError::Write(e.to_string())), 121 | } 122 | } 123 | 124 | /// Write using ptrace and opening /proc/pid/mem directly. 125 | /// Used for protected "read-only" pages. 126 | fn write_protected(pid: &Pid, at: u64, data: &Vec) -> Result { 127 | nix::sys::ptrace::attach(*pid)?; 128 | 129 | let mut f = File::options() 130 | .read(true) 131 | .write(true) 132 | .open(format!("/proc/{}/mem", pid.as_raw())) 133 | .unwrap(); 134 | 135 | f.seek(SeekFrom::Start(at))?; 136 | let r = f.write(data); 137 | 138 | nix::sys::ptrace::detach(*pid, Some(nix::sys::signal::Signal::SIGCONT))?; 139 | r 140 | } 141 | -------------------------------------------------------------------------------- /src/pattern.rs: -------------------------------------------------------------------------------- 1 | use serde::de::Error; 2 | use serde::de::{Deserialize, Deserializer}; 3 | use std::fmt; 4 | use std::num::ParseIntError; 5 | use std::ops::Index; 6 | use std::slice::Iter; 7 | 8 | pub struct Pattern { 9 | string: String, 10 | bytes: Vec>, 11 | } 12 | 13 | impl Pattern { 14 | pub fn new(pattern: String) -> Result { 15 | Ok(Self { 16 | bytes: as_bytes(&pattern)?, 17 | string: pattern, 18 | }) 19 | } 20 | 21 | pub fn len(&self) -> usize { 22 | self.bytes.len() 23 | } 24 | 25 | pub fn iter(&self) -> Iter<'_, Option> { 26 | self.bytes.iter() 27 | } 28 | } 29 | 30 | impl fmt::Display for Pattern { 31 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 32 | write!(f, "{}", self.string) 33 | } 34 | } 35 | 36 | impl Index for Pattern { 37 | type Output = Option; 38 | 39 | fn index(&self, idx: usize) -> &Self::Output { 40 | &self.bytes[idx] 41 | } 42 | } 43 | 44 | pub fn as_bytes(pattern: &str) -> Result>, ParseIntError> { 45 | let mut bytes = vec![]; 46 | for hex in pattern.split(' ') { 47 | bytes.push(match hex { 48 | // __ is considered ignored 49 | "__" => None, 50 | _ => Some(u8::from_str_radix(hex, 16)?), 51 | }); 52 | } 53 | 54 | Ok(bytes) 55 | } 56 | 57 | pub fn deserialize<'de, D>(deserializer: D) -> Result 58 | where 59 | D: Deserializer<'de>, 60 | { 61 | let pattern = String::deserialize(deserializer)?; 62 | Pattern::new(pattern).map_err(Error::custom) 63 | } 64 | -------------------------------------------------------------------------------- /src/trainer.rs: -------------------------------------------------------------------------------- 1 | pub mod daemon; 2 | 3 | use crate::display; 4 | use crate::error::TrainerError; 5 | use crate::Hack; 6 | use daemon::Daemon; 7 | use procfs::{process, process::Process}; 8 | use serde::Deserialize; 9 | use std::error::Error; 10 | use std::path::{Path, PathBuf}; 11 | 12 | #[derive(Deserialize)] 13 | pub struct Trainer { 14 | pub name: String, 15 | pub version: String, 16 | pub process: String, 17 | #[serde(rename = "feature")] 18 | pub features: Vec, 19 | pub daemon: Option, 20 | pub enable: bool, 21 | } 22 | 23 | impl Trainer { 24 | pub fn run(&self, full: bool, display: bool) { 25 | display.then(|| display::trainer(&self)); 26 | 27 | let proc = self.probe(); 28 | if let Ok(proc) = proc { 29 | for feature in &self.features { 30 | if feature.enable { 31 | display.then(|| display::feature(feature)); 32 | let res = feature.run(&proc, full); 33 | display.then(|| display::feature_result(&feature, &res, full)); 34 | } 35 | } 36 | } else if let Err(e) = proc { 37 | display.then(|| display::trainer_err(&e)); 38 | } 39 | } 40 | 41 | pub fn probe(&self) -> Result { 42 | let procs = process::all_processes()?; 43 | for proc in procs { 44 | if proc.stat.comm == self.process { 45 | return Ok(proc); 46 | } 47 | } 48 | 49 | Err(TrainerError::Process(format!( 50 | "Could not find process by '{}'.", 51 | self.process 52 | )))? 53 | } 54 | 55 | pub fn features(&self) -> &Vec { 56 | return &self.features; 57 | } 58 | } 59 | 60 | impl TryFrom for Trainer { 61 | type Error = Box; 62 | 63 | fn try_from(path: PathBuf) -> Result { 64 | let trainer_str = std::fs::read_to_string(path)?; 65 | let trainer: Trainer = toml::from_str(&trainer_str)?; 66 | Ok(trainer) 67 | } 68 | } 69 | 70 | pub fn from_dir(dir: &Path) -> Result, Box> { 71 | let mut trainers = vec![]; 72 | 73 | let files = dir 74 | .read_dir()? 75 | .filter_map(|r| r.ok()) 76 | .map(|e| e.path()) 77 | .filter(|p| p.is_file()); 78 | 79 | for file in files { 80 | trainers.push(file.clone().try_into()?); 81 | } 82 | 83 | Ok(trainers) 84 | } 85 | 86 | pub fn from_args( 87 | dir: &Path, 88 | trainer: Option, 89 | trainer_file: Option, 90 | ) -> Result, Box> { 91 | Ok(if let Some(trainer) = trainer { 92 | vec![dir.join(format!("{}.toml", trainer)).try_into()?] 93 | } else if let Some(file) = trainer_file { 94 | vec![file.try_into()?] 95 | } else { 96 | from_dir(dir)? 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /src/trainer/daemon.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | /// Daemon options for trainer. 4 | #[derive(Deserialize)] 5 | pub struct Daemon { 6 | pub delay: Option, 7 | } 8 | -------------------------------------------------------------------------------- /trainers/darksouls3.toml: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------- 2 | # NOTICE: I DO NOT RECOMMEND USING THIS WHILE PLAYING ONLINE. 3 | # ..but you'll probably be okay. 4 | # 5 | # The memory regions in this trainer are not tightened, so it may not be crazy fast 6 | # at finding the pattern. This is because it apparently moves around a lot depending 7 | # on different wine/proton builds. However, TuxTrain will print the correct region 8 | # once found- so set it manually if you want it to be more efficient. 9 | # 10 | # CREDITS: 11 | # The current hexadecimal pattern was found with the help of debugging this mod: 12 | # https://github.com/0dm/DS3DebugFPS 13 | # ----------------------------------------------------------------------- 14 | name = "Dark Souls III Trainer" 15 | version = '0.0.1' 16 | process = "DarkSoulsIII.ex" # Kernel cutoff fun 17 | enable = false 18 | 19 | [daemon] 20 | delay = 2000 21 | 22 | # NOTICE: You need to play in windowed mode, because there's also a 23 | # refresh rate cap at 60Hz in fullscreen, which goes away in windowed mode. 24 | # 25 | # Unfortunately the game engine isn't built for anything higher than 60, so this 26 | # may or may not cause weirdness such as slow rolls/and or inability to run at times. 27 | # 28 | # One interesting note, the default here is "technically" 30 FPS (F0 41), but because 29 | # of the next hexadecimal byte being "00" this fps limit isn't used, so it's instead 30 | # capped at 60 FPS for a different reason. Changing "00" to "01" enables this limiter. 31 | # 32 | # Pick your poison here: 33 | # "00 00 F0 41" - 30 FPS (console-like, yuck) 34 | # "00 00 70 42" - 60 FPS (but why?) 35 | # "00 00 8C 42" - 70 FPS 36 | # "00 00 A0 42" - 80 FPS 37 | # "00 00 B4 42" - 90 FPS (probably the most balanced, but don't fear trying higher) 38 | # "00 00 C8 42" - 100 FPS 39 | # "00 00 DC 42" - 110 FPS 40 | # "00 00 F0 42" - 120 FPS 41 | # "00 00 02 43" - 130 FPS 42 | # "00 00 10 43" - 144 FPS (default, works okay with some occasional issues) 43 | # "00 00 7A 44" - 1000 FPS (only the future will tell) 44 | # "00 40 9C 45" - 5000 FPS (dude?) 45 | # 46 | # ..Or make your own, they're just floats in hexadecimal. 47 | [[feature]] 48 | name = "FPS Unlock" 49 | # region = [140735844279204, 140735844279210] 50 | pattern = "00 00 F0 41 00 FE" 51 | replace = "00 00 10 43 01 __" 52 | enable = false 53 | -------------------------------------------------------------------------------- /trainers/eldenring.toml: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------- 2 | # NOTICE: DO NOT RUN THIS WITH EAC ENABLED. 3 | # 4 | # TuxTrain is generic and it doesn't care what you are doing, therefor 5 | # it does NOT care about something like EAC. I've not yet heard anyone 6 | # neither try nor getting banned for unlocking their FPS, but I don't 7 | # think you want to be the first either. It is also unfair when others 8 | # are forced to play at 60 FPS. 9 | # 10 | # The regions in this trainer are fully tightened, I am not entirely sure 11 | # if they are the same for different versions/releases of Elden Ring. 12 | # 13 | # If tuxtrain cannot find the pattern, please create an issue! 14 | # See below for how to find the correct region for your version: 15 | # 16 | # - Either remove the region entirely to search the entire process 17 | # memory (this may take some time), tuxtrain will then output the 18 | # correct region once found. 19 | # 20 | # - Or manually try each map (based on /proc/pid/maps), they should 21 | # look similar to below. Convert the addresses to decimal 22 | # (e.g. 140000000-140001000 for [start, end]), it's likely the ones close 23 | # in number to the preset regions in this file. 24 | # ---------------- 25 | # 140000000-140001000 rwxp 00000000 103:02 3288569595 /home/user/games/elden-ring/drive_c/Games/ELDEN RING/Game/eldenring.exe 26 | # 140001000-14452b000 rwxp 00000000 00:00 0 27 | # 14452b000-1457c9000 rwxp 00000000 00:00 0 28 | # 16fff0000-16fff1000 rwxp 00000000 00:00 0 29 | # ---------------- 30 | # 31 | # CREDITS: 32 | # The current hexadecimal patterns were taken from: 33 | # - https://github.com/uberhalit/EldenRingFpsUnlockAndMore 34 | # - https://github.com/gurrgur/er-patcher 35 | # - https://github.com/techiew/EldenRingMods 36 | # ----------------------------------------------------------------------- 37 | name = "Elden Ring Trainer" 38 | version = '0.0.4' 39 | process = "eldenring.exe" 40 | enable = false 41 | 42 | [daemon] 43 | delay = 2000 44 | 45 | [[feature]] 46 | name = "FPS Unlock" 47 | region = [5383384927, 5383384935] 48 | pattern = "C7 __ __ __ 88 88 3C EB" 49 | replace = "__ __ __ 60 0B 36 3B __" 50 | enable = false 51 | 52 | # NOTICE: 53 | # This does not modify your hertz, it only stops Elden Ring from doing it. 54 | # In order to stop Elden Ring from actually forcing your monitor 55 | # (on X11) to 60hz, make sure to either run the daemon (tuxtraind) 56 | # or start tuxtrain quickly after launching the game. 57 | [[feature]] 58 | name = "Hz Unlock" 59 | region = [5395294933, 5395294949] 60 | pattern = "EB __ C7 __ __ 3C 00 00 00 C7 __ __ 01 00 00 00" 61 | replace = "__ __ __ __ __ 00 __ __ __ __ __ __ 00 __ __ __" 62 | enable = false 63 | 64 | # Custom resolution 65 | # By default replaces the 1920x1080 option with 3440x1440 where: 66 | # "70 0D" is 3440 but flipped 67 | # "A0 05" is 1440 but flipped 68 | # 69 | # To customize the resolution: 70 | # 1. Convert your custom width and height to hexadecimal 71 | # 2. Flip the two hexadecimal bytes, e.g. "07 80" for 1920 becomes "80 07". 72 | [[feature]] 73 | name = "Custom Resolution" 74 | region = [5429833656, 5429833672] 75 | #pattern = "00 05 00 00 D0 02 00 00 A0 05 00 00 2A 03 00 00" # Option 1280x720 using "00 05" and "D0 02" 76 | pattern = "80 07 00 00 38 04 00 00 00 08 00 00 80 04 00 00" # Option 1920x1080 using "80 07" and "38 04" 77 | replace = "70 0D __ __ A0 05 __ __ __ __ __ __ __ __ __ __" # Replace with 3440x1440 using "70 0D" and "A0 05" 78 | enable = false 79 | 80 | [[feature]] 81 | name = "Aspect Ratio Unlock" 82 | # region = [5395580718, 5395580724] 83 | pattern = "74 4f 45 8b 94 CC" 84 | replace = "EB __ __ __ __ __" 85 | enable = false 86 | 87 | # Increases animation distance 88 | # May also fix low frames for enemy animations when: 89 | # - using ultrawide resolutions 90 | # - enemies are outside the regular 16:9 screen space 91 | [[feature]] 92 | name = "Animation Distance Boost" 93 | region = [5379914000, 5379914030] 94 | pattern = "E8 __ __ __ __ 0F 28 __ 0F 28 __ E8 __ __ __ __ F3 0F __ __ 0F 28 __ F3 41 0F 5E 4C 24 54" 95 | replace = "__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ 0F 57 C9 66 0F EF C9" 96 | enable = false 97 | 98 | [[feature]] 99 | name = "Remove Chromatic Aberration" 100 | pattern = "0F 11 __ 60 __ 8D __ 80 00 00 00 0F 10 __ A0 00 00 00 0F 11 __ F0 __ 8D __ B0 00 00 00 0F 10 __ 0F 11 __ 0F 10 __ 10 __ __ __ __ __ __ __ __ 0F 11 __ __" 101 | replace = "__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ 66 0F EF C9" 102 | region = [5397271058, 5397271109] 103 | enable = false 104 | 105 | [[feature]] 106 | name = "Remove Vignette" 107 | pattern = "F3 0F 10 __ 50 F3 0F 59 __ __ __ __ __ E8 __ __ __ __ F3 __ 0F 5C __ F3 __ 0F 59 __ __ 8D __ __ A0 00 00 00" 108 | replace = "__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ 0F 5C C0 90 __ __ __ __ __ __ __ __" 109 | region = [5397270341, 5397270377] 110 | enable = false 111 | -------------------------------------------------------------------------------- /trainers/example.toml: -------------------------------------------------------------------------------- 1 | # Name and version of trainer, this has no significance other than output 2 | name = "My Trainer" 3 | version = '0.0.1' 4 | 5 | # Based on /proc/pid/comm which is usually always the executable. 6 | # This is used when probing for a process. 7 | # TODO: Might add more ways to probe if needed 8 | process = "someprocess" 9 | 10 | # Set to false to disable trainer 11 | enable = true 12 | 13 | # Settings for the daemon only 14 | [daemon] 15 | # When a matching process is discovered, delay by (ms). 16 | # Useful for when the important memory addresses don't exist yet, or 17 | # if you need to wait until something happens before running the trainer. 18 | # TODO: This may be useful for features as well 19 | delay = 2000 20 | 21 | # A feature in the trainer 22 | [[feature]] 23 | name = "FPS Unlock" 24 | 25 | # Memory address region, based on /proc/pid/maps but in decimal. 26 | # 27 | # If you don't define a region at all it will search through the full 28 | # process memory. This can be slow depending on how much memory the process 29 | # is using, so consider tightening the region, more on that below: 30 | # 31 | # When tuxtrain finds the pattern it will output the exact address range 32 | # and you can consider tightening it here. However, it may not always 33 | # work to tighten it fully, because for some programs, the pattern may 34 | # not always appear in the same region. 35 | region = [7123045, 9124418] 36 | 37 | # Hexadecimal patterns, this is what tuxtrain will search for and 38 | # replace within the desired region. This might be a really good time 39 | # to finally bring out your monospace fonts to match up these patterns, 40 | # the perfect alignment lets you know exactly what will be replaced. 41 | # FYI: There's a reason it's called "pattern" instead of "search", heh. 42 | # 43 | # See below explanation: 44 | # 45 | # => pattern: 46 | # __ will be ignored during search, a must have for variable memory. 47 | # 48 | # However, it's worth noting that the first hex number cannot be "__". 49 | # Well, that would also be completely useless of course. 50 | # 51 | # => replace: 52 | # __ will be ignored when writing to memory. 53 | # 54 | # But, technically it merges "replace" with the original data found 55 | # after search- that is, "__" becomes whatever was found in memory. 56 | # Then, it simply overwrites using the full range of bytes, saves 57 | # a few memory write operations. 58 | pattern = "FF __ CB 00 7F __ __ D0" 59 | replace = "__ __ 9F 5C __ 58 88 __" 60 | 61 | # Set to false to disable feature 62 | enable = true 63 | 64 | # Another feature in the trainer 65 | [[feature]] 66 | name = "Infinite Ammo" 67 | region = [52030, 73204] 68 | pattern = "FF 00 __ 00 __ __ 7F 58 D0" 69 | replace = "__ __ __ __ 0B 4C __ __ __" 70 | enable = true 71 | -------------------------------------------------------------------------------- /trainers/sekiro.toml: -------------------------------------------------------------------------------- 1 | name = "Sekiro Trainer" 2 | version = '0.0.3' 3 | process = "sekiro.exe" 4 | enable = false 5 | 6 | [daemon] 7 | delay=2000 8 | 9 | # Pattern taken from Sekiro FPS Unlock And More project: 10 | # https://github.com/uberhalit/SekiroFpsUnlockAndMore/blob/d6312c6b0af0bcdf987568e1490b7d842548ae99/SekiroFpsUnlockAndMore/GameData.cs#L28 11 | # Hex values calculated using the following python script: 12 | # import struct 13 | # struct.pack('