forked from ReScrap/ScrapHacks
Compare commits
6 commits
Author | SHA1 | Date | |
---|---|---|---|
63962c95cc | |||
58407ecc9f | |||
8e0df74541 | |||
45f38885ec | |||
b5afe0e2a5 | |||
a93a2fc63a |
62 changed files with 11051 additions and 0 deletions
24
scrapper_web/.gitignore
vendored
Normal file
24
scrapper_web/.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
3
scrapper_web/.vscode/extensions.json
vendored
Normal file
3
scrapper_web/.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["svelte.svelte-vscode"]
|
||||
}
|
47
scrapper_web/README.md
Normal file
47
scrapper_web/README.md
Normal file
|
@ -0,0 +1,47 @@
|
|||
# Svelte + Vite
|
||||
|
||||
This template should help get you started developing with Svelte in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
|
||||
|
||||
## Need an official Svelte framework?
|
||||
|
||||
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
|
||||
|
||||
## Technical considerations
|
||||
|
||||
**Why use this over SvelteKit?**
|
||||
|
||||
- It brings its own routing solution which might not be preferable for some users.
|
||||
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
|
||||
|
||||
This template contains as little as possible to get started with Vite + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
|
||||
|
||||
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
|
||||
|
||||
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
|
||||
|
||||
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
|
||||
|
||||
**Why include `.vscode/extensions.json`?**
|
||||
|
||||
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
|
||||
|
||||
**Why enable `checkJs` in the JS template?**
|
||||
|
||||
It is likely that most cases of changing variable types in runtime are likely to be accidental, rather than deliberate. This provides advanced typechecking out of the box. Should you like to take advantage of the dynamically-typed nature of JavaScript, it is trivial to change the configuration.
|
||||
|
||||
**Why is HMR not preserving my local component state?**
|
||||
|
||||
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
|
||||
|
||||
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
|
||||
|
||||
```js
|
||||
// store.js
|
||||
// An extremely simple external store
|
||||
import { writable } from 'svelte/store'
|
||||
export default writable(0)
|
||||
```
|
13
scrapper_web/index.html
Normal file
13
scrapper_web/index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Svelte</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
33
scrapper_web/jsconfig.json
Normal file
33
scrapper_web/jsconfig.json
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "Node",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
/**
|
||||
* svelte-preprocess cannot figure out whether you have
|
||||
* a value or a type, so tell TypeScript to enforce using
|
||||
* `import type` instead of `import` for Types.
|
||||
*/
|
||||
"importsNotUsedAsValues": "error",
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* To have warnings / errors of the Svelte compiler at the
|
||||
* correct position, enable source maps by default.
|
||||
*/
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable this if you'd like to use dynamic types.
|
||||
*/
|
||||
"checkJs": false
|
||||
},
|
||||
/**
|
||||
* Use global.d.ts instead of compilerOptions.types
|
||||
* to avoid limiting type declarations.
|
||||
*/
|
||||
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
}
|
26
scrapper_web/package.json
Normal file
26
scrapper_web/package.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "scrapper_web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "wasm-pack build ./scrapper -t web && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^2.0.2",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"cssnano": "^5.1.14",
|
||||
"cssnano-preset-advanced": "^5.3.9",
|
||||
"daisyui": "^2.50.0",
|
||||
"filedrop-svelte": "^0.1.2",
|
||||
"postcss": "^8.4.21",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte-preprocess": "^5.0.1",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"vite": "^4.1.0",
|
||||
"vite-plugin-wasm-pack": "^0.1.12"
|
||||
}
|
||||
}
|
1777
scrapper_web/pnpm-lock.yaml
Normal file
1777
scrapper_web/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
11
scrapper_web/postcss.config.cjs
Normal file
11
scrapper_web/postcss.config.cjs
Normal file
|
@ -0,0 +1,11 @@
|
|||
let cssnano_plugin = {};
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
cssnano_plugin = { cssnano: { preset: "advanced" } };
|
||||
}
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
...cssnano_plugin,
|
||||
},
|
||||
};
|
1
scrapper_web/public/vite.svg
Normal file
1
scrapper_web/public/vite.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
14
scrapper_web/scrapper/.gitignore
vendored
Normal file
14
scrapper_web/scrapper/.gitignore
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
31
scrapper_web/scrapper/Cargo.toml
Normal file
31
scrapper_web/scrapper/Cargo.toml
Normal file
|
@ -0,0 +1,31 @@
|
|||
[package]
|
||||
name = "scrapper"
|
||||
version = "0.1.0"
|
||||
authors = []
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
aes = "0.8.2"
|
||||
anyhow = "1.0.69"
|
||||
binrw = "0.11.1"
|
||||
cbc = "0.1.2"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
derivative = "2.2.0"
|
||||
js-sys = "0.3.61"
|
||||
pelite = "0.10.0"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
serde-wasm-bindgen = "0.4.5"
|
||||
wasm-bindgen = "0.2.83"
|
||||
wasm-bindgen-file-reader = "1.0.0"
|
||||
web-sys = { version = "0.3.61", features = ["File", "BlobPropertyBag", "Blob", "Url"] }
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = ["-O4"]
|
23
scrapper_web/scrapper/README.md
Normal file
23
scrapper_web/scrapper/README.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
# scrapper
|
||||
|
||||
## Usage
|
||||
|
||||
[rsw-rs doc](https://github.com/lencx/rsw-rs)
|
||||
|
||||
```bash
|
||||
# install rsw
|
||||
cargo install rsw
|
||||
|
||||
# --- help ---
|
||||
# rsw help
|
||||
rsw -h
|
||||
# new help
|
||||
rsw new -h
|
||||
|
||||
# --- usage ---
|
||||
# dev
|
||||
rsw watch
|
||||
|
||||
# production
|
||||
rsw build
|
||||
```
|
155
scrapper_web/scrapper/src/lib.rs
Normal file
155
scrapper_web/scrapper/src/lib.rs
Normal file
|
@ -0,0 +1,155 @@
|
|||
use binrw::{binread, BinReaderExt};
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_file_reader::WebSysFile;
|
||||
use web_sys::{Blob, File};
|
||||
|
||||
type JsResult<T> = Result<T,JsValue>;
|
||||
|
||||
#[binread]
|
||||
#[derive(Serialize, Debug)]
|
||||
struct ScrapFile {
|
||||
#[br(temp)]
|
||||
name_len: u32,
|
||||
#[br(count = name_len)]
|
||||
#[br(map = |s: Vec<u8>| String::from_utf8_lossy(&s).to_string())]
|
||||
path: String,
|
||||
size: u32,
|
||||
offset: u32,
|
||||
}
|
||||
|
||||
#[binread]
|
||||
#[br(magic = b"BFPK", little)]
|
||||
#[derive(Serialize, Debug)]
|
||||
struct PackedHeader {
|
||||
version: u32,
|
||||
#[br(temp)]
|
||||
num_files: u32,
|
||||
#[br(count= num_files)]
|
||||
files: Vec<ScrapFile>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum DirectoryTree {
|
||||
File {
|
||||
size: u32,
|
||||
offset: u32,
|
||||
file_index: u8,
|
||||
},
|
||||
Directory {
|
||||
entries: BTreeMap<String, DirectoryTree>,
|
||||
},
|
||||
}
|
||||
|
||||
#[wasm_bindgen(inspectable)]
|
||||
pub struct MultiPack {
|
||||
files: Vec<(String,WebSysFile)>,
|
||||
tree: DirectoryTree,
|
||||
}
|
||||
|
||||
fn blob_url(buffer: &[u8]) -> JsResult<String> {
|
||||
let uint8arr =
|
||||
js_sys::Uint8Array::new(&unsafe { js_sys::Uint8Array::view(buffer) }.into());
|
||||
let array = js_sys::Array::new();
|
||||
array.push(&uint8arr.buffer());
|
||||
let blob = Blob::new_with_u8_array_sequence_and_options(
|
||||
&array,
|
||||
web_sys::BlobPropertyBag::new().type_("application/octet-stream"),
|
||||
)
|
||||
.unwrap();
|
||||
web_sys::Url::create_object_url_with_blob(&blob)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl MultiPack {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn parse(files: Vec<File>) -> Self {
|
||||
let mut tree = DirectoryTree::default();
|
||||
let mut web_files = vec![];
|
||||
for (file_index, file) in files.into_iter().enumerate() {
|
||||
let file_name = file.name();
|
||||
let mut fh = WebSysFile::new(file);
|
||||
let header = fh.read_le::<PackedHeader>().unwrap();
|
||||
tree.merge(&header.files, file_index.try_into().unwrap());
|
||||
web_files.push((file_name,fh));
|
||||
}
|
||||
Self {
|
||||
tree,
|
||||
files: web_files,
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn tree(&self) -> JsValue {
|
||||
serde_wasm_bindgen::to_value(&self.tree).unwrap()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn download(
|
||||
&mut self,
|
||||
file_index: u8,
|
||||
offset: u32,
|
||||
size: u32,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
let Some((_,file)) = self.files.get_mut(file_index as usize) else {
|
||||
return Err("File not found".into());
|
||||
};
|
||||
let mut buffer = vec![0u8; size as usize];
|
||||
file.seek(SeekFrom::Start(offset as u64))
|
||||
.map_err(|e| format!("Failed to seek file: {e}"))?;
|
||||
file.read(&mut buffer)
|
||||
.map_err(|e| format!("Failed to read from file: {e}"))?;
|
||||
Ok(blob_url(&buffer)?.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DirectoryTree {
|
||||
fn default() -> Self {
|
||||
Self::Directory {
|
||||
entries: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectoryTree {
|
||||
fn add_child(&mut self, name: &str, node: Self) -> &mut Self {
|
||||
match self {
|
||||
Self::File { .. } => panic!("Can't add child to file!"),
|
||||
Self::Directory {
|
||||
entries
|
||||
} => entries.entry(name.to_owned()).or_insert(node),
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, files: &[ScrapFile], file_index: u8) {
|
||||
for file in files {
|
||||
let mut folder = &mut *self;
|
||||
let path: Vec<_> = file.path.split('/').collect();
|
||||
if let Some((filename, path)) = path.as_slice().split_last() {
|
||||
for part in path {
|
||||
let DirectoryTree::Directory { entries } = folder else {
|
||||
unreachable!();
|
||||
};
|
||||
folder = entries.entry(part.to_string()).or_default();
|
||||
}
|
||||
folder.add_child(
|
||||
filename,
|
||||
DirectoryTree::File {
|
||||
size: file.size,
|
||||
offset: file.offset,
|
||||
file_index,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn main() -> Result<(), JsValue> {
|
||||
console_error_panic_hook::set_once();
|
||||
Ok(())
|
||||
}
|
13
scrapper_web/src/App.svelte
Normal file
13
scrapper_web/src/App.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script>
|
||||
import Explorer from "./lib/Explorer.svelte";
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<div>
|
||||
<h1>Scrapland .packed explorer</h1>
|
||||
<Explorer />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
</style>
|
109
scrapper_web/src/app.pcss
Normal file
109
scrapper_web/src/app.pcss
Normal file
|
@ -0,0 +1,109 @@
|
|||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
li {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
.lds-dual-ring {
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.lds-dual-ring:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 8px;
|
||||
border-radius: 50%;
|
||||
border: 6px solid #fff;
|
||||
border-color: #fff transparent #fff transparent;
|
||||
animation: lds-dual-ring 1.2s linear infinite;
|
||||
}
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
52
scrapper_web/src/lib/Explorer.svelte
Normal file
52
scrapper_web/src/lib/Explorer.svelte
Normal file
|
@ -0,0 +1,52 @@
|
|||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import TreeView from "./TreeView.svelte";
|
||||
import ScrapWorker from "../scrapper.worker?worker";
|
||||
let worker;
|
||||
let tree;
|
||||
let busy;
|
||||
busy = false;
|
||||
onMount(async () => {
|
||||
worker = new ScrapWorker();
|
||||
worker.onmessage = (msg) => {
|
||||
console.log({ msg });
|
||||
if (msg.data) {
|
||||
if (msg.data.parse) {
|
||||
tree = msg.data.parse;
|
||||
busy = false;
|
||||
}
|
||||
if (msg.data.download) {
|
||||
let [file_name, url] = msg.data.download;
|
||||
let dl = document.createElement("a");
|
||||
dl.href = url;
|
||||
dl.download = file_name;
|
||||
dl.click();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
let files;
|
||||
function process() {
|
||||
console.log({ files });
|
||||
busy = true;
|
||||
worker.postMessage({ parse: files });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class:lds-dual-ring={busy}>
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
accept=".packed"
|
||||
class="file-input file-input-bordered w-full max-w-xs"
|
||||
disabled={busy}
|
||||
bind:files
|
||||
on:change={process}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if tree}
|
||||
{#each [...tree.entries] as [name, child]}
|
||||
<TreeView scrap={worker} label={name} tree={child} />
|
||||
{/each}
|
||||
{/if}
|
56
scrapper_web/src/lib/TreeView.svelte
Normal file
56
scrapper_web/src/lib/TreeView.svelte
Normal file
|
@ -0,0 +1,56 @@
|
|||
|
||||
<script>
|
||||
export let tree;
|
||||
export let scrap;
|
||||
export let label=undefined;
|
||||
let expanded = false;
|
||||
function toggleExpansion() {
|
||||
expanded = !expanded;
|
||||
};
|
||||
function download() {
|
||||
console.log({label,tree});
|
||||
scrap.postMessage({download:{label,...tree}});
|
||||
console.log(tree);
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
{#if tree.type == "directory" && tree.entries}
|
||||
<span on:click={toggleExpansion} on:keydown={toggleExpansion}>
|
||||
{#if expanded}
|
||||
<span class="arrow">[-]</span>
|
||||
{:else}
|
||||
<span class="arrow">[+]</span>
|
||||
{/if}
|
||||
{label}
|
||||
</span>
|
||||
{#if tree.entries && expanded}
|
||||
{#each [...tree.entries] as [name, child]}
|
||||
<svelte:self {scrap} label={name} tree={child} />
|
||||
{/each}
|
||||
{/if}
|
||||
{:else}
|
||||
<span>
|
||||
<span class="no-arrow" />
|
||||
<a href="#download" title="{tree.size} bytes" on:click={download}>{label}</a>
|
||||
</span>
|
||||
{/if}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
ul {
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
padding-left: 1.2rem;
|
||||
user-select: none;
|
||||
}
|
||||
.no-arrow {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
.arrow {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
6
scrapper_web/src/main.js
Normal file
6
scrapper_web/src/main.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import './app.pcss'
|
||||
import App from './App.svelte'
|
||||
|
||||
export default new App({
|
||||
target: document.getElementById('app'),
|
||||
});
|
28
scrapper_web/src/scrapper.worker.js
Normal file
28
scrapper_web/src/scrapper.worker.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import wasm, { MultiPack } from "scrapper";
|
||||
|
||||
async function initialize() {
|
||||
await wasm();
|
||||
let pack;
|
||||
let handlers = {
|
||||
parse(data) {
|
||||
pack = new MultiPack(data);
|
||||
return pack.tree();
|
||||
},
|
||||
download(data) {
|
||||
if (pack) {
|
||||
let { label, file_index, offset, size } = data;
|
||||
return [label, pack.download(file_index, offset, size)];
|
||||
}
|
||||
},
|
||||
};
|
||||
self.onmessage = (event) => {
|
||||
for (var [name, func] of Object.entries(handlers)) {
|
||||
let data = event.data[name];
|
||||
if (data) {
|
||||
postMessage(Object.fromEntries([[name, func(data)]]));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
initialize();
|
2
scrapper_web/src/vite-env.d.ts
vendored
Normal file
2
scrapper_web/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
6
scrapper_web/svelte.config.js
Normal file
6
scrapper_web/svelte.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
36
scrapper_web/tailwind.config.cjs
Normal file
36
scrapper_web/tailwind.config.cjs
Normal file
|
@ -0,0 +1,36 @@
|
|||
module.exports = {
|
||||
content: ["./src/**/*.{svelte,js,ts}"],
|
||||
plugins: [require("@tailwindcss/forms"),require("daisyui")],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
},
|
||||
},
|
||||
daisyui: {
|
||||
styled: true,
|
||||
themes: true,
|
||||
base: true,
|
||||
utils: true,
|
||||
logs: true,
|
||||
rtl: false,
|
||||
prefix: "",
|
||||
darkTheme: "scraptool",
|
||||
themes: [
|
||||
{
|
||||
scraptool: {
|
||||
primary: "#F28C18",
|
||||
secondary: "#b45309",
|
||||
accent: "#22d3ee",
|
||||
neutral: "#1B1D1D",
|
||||
"base-100": "#212121",
|
||||
info: "#2463EB",
|
||||
success: "#16A249",
|
||||
warning: "#DB7706",
|
||||
error: "#DC2828",
|
||||
// "--rounded-box": "0.4rem",
|
||||
// "--rounded-btn": "0.2rem"
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
10
scrapper_web/vite.config.js
Normal file
10
scrapper_web/vite.config.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import wasmPack from 'vite-plugin-wasm-pack';
|
||||
import preprocess from 'svelte-preprocess';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [wasmPack("./scrapper/"),svelte({
|
||||
preprocess: preprocess({ postcss: true })
|
||||
})]
|
||||
});
|
134
tools/ghidra_scripts/Scrap_analyze.py
Normal file
134
tools/ghidra_scripts/Scrap_analyze.py
Normal file
|
@ -0,0 +1,134 @@
|
|||
|
||||
try:
|
||||
import ghidra_bridge
|
||||
has_bridge=True
|
||||
except ImportError:
|
||||
has_bridge=False
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
if has_bridge:
|
||||
import ghidra_bridge
|
||||
b = ghidra_bridge.GhidraBridge(namespace=globals(), hook_import=True)
|
||||
@contextmanager
|
||||
def transaction():
|
||||
start()
|
||||
try:
|
||||
yield
|
||||
except Exception as e:
|
||||
end(False)
|
||||
raise e
|
||||
end(True)
|
||||
else:
|
||||
@contextmanager
|
||||
def transaction():
|
||||
yield
|
||||
|
||||
import ghidra.program.model.symbol.SymbolType as SymbolType
|
||||
import ghidra.program.model.symbol.SourceType as SourceType
|
||||
from ghidra.app.cmd.label import CreateNamespacesCmd
|
||||
from ghidra.program.model.data.DataUtilities import createData
|
||||
from ghidra.program.model.data.DataUtilities import ClearDataMode
|
||||
from ghidra.program.model.listing.CodeUnit import PLATE_COMMENT
|
||||
def make_namespace(parts):
|
||||
ns_cmd = CreateNamespacesCmd("::".join(parts), SourceType.USER_DEFINED)
|
||||
ns_cmd.applyTo(currentProgram)
|
||||
return ns_cmd.getNamespace()
|
||||
|
||||
|
||||
callback_refs = [ref.fromAddress for ref in getReferencesTo(toAddr(0x590C70)).tolist()]
|
||||
engine_var_refs = [
|
||||
ref.fromAddress for ref in getReferencesTo(toAddr(0x5319D0)).tolist()
|
||||
]
|
||||
|
||||
dtm = currentProgram.getDataTypeManager()
|
||||
engine_var_dt = dtm.getDataType("/EngineVar")
|
||||
callback_dt = dtm.getDataType("/CCallback")
|
||||
|
||||
def create_data(addr,dtype):
|
||||
return createData(currentProgram,addr,dtype,0,False,ClearDataMode.CLEAR_ALL_CONFLICT_DATA)
|
||||
|
||||
def create_str(addr):
|
||||
str_len = (findBytes(addr, b"\0").offset - addr.offset) + 1
|
||||
clearListing(addr, addr.add(str_len))
|
||||
return createAsciiString(addr)
|
||||
|
||||
|
||||
def make_namespace(parts):
|
||||
ns_cmd = CreateNamespacesCmd("::".join(parts), SourceType.USER_DEFINED)
|
||||
ns_cmd.applyTo(currentProgram)
|
||||
return ns_cmd.getNamespace()
|
||||
|
||||
|
||||
def get_call_obj(addr):
|
||||
func = getFunctionContaining(addr)
|
||||
if func is None:
|
||||
disassemble(addr)
|
||||
func = createFunction(addr,None)
|
||||
call_obj = {"this": None, "stack": []}
|
||||
for inst in currentProgram.listing.getInstructions(func.body, True):
|
||||
affected_objs = [r.toString() for r in inst.resultObjects.tolist()]
|
||||
inst_name = inst.getMnemonicString()
|
||||
if inst_name == "PUSH":
|
||||
val=inst.getScalar(0)
|
||||
if val is not None:
|
||||
call_obj["stack"].insert(0, toAddr(val.getValue()).toString())
|
||||
elif inst_name == "MOV" and "ECX" in affected_objs:
|
||||
this = inst.getScalar(1)
|
||||
if this is not None:
|
||||
call_obj["this"] = toAddr(this.getValue()).toString()
|
||||
elif inst_name == "CALL":
|
||||
break
|
||||
return func, call_obj
|
||||
|
||||
|
||||
with transaction():
|
||||
for ref in callback_refs:
|
||||
register_callback, call_obj = get_call_obj(ref)
|
||||
name, addr = call_obj["stack"]
|
||||
this = toAddr(call_obj["this"])
|
||||
addr = toAddr(addr)
|
||||
name = create_str(toAddr(name)).getValue()
|
||||
callback_ns = make_namespace(["Callbacks"])
|
||||
ns = make_namespace(["Callbacks", name])
|
||||
clearListing(addr)
|
||||
disassemble(addr)
|
||||
func = createFunction(addr,None)
|
||||
print(name,func)
|
||||
createLabel(addr, name, callback_ns, True, SourceType.USER_DEFINED)
|
||||
createLabel(
|
||||
register_callback.getEntryPoint(),
|
||||
"register",
|
||||
ns,
|
||||
True,
|
||||
SourceType.USER_DEFINED,
|
||||
)
|
||||
createLabel(this, name, None, True, SourceType.USER_DEFINED)
|
||||
create_data(this,callback_dt)
|
||||
|
||||
for ref in engine_var_refs:
|
||||
register_engine_var, call_obj = get_call_obj(ref)
|
||||
engine_var = call_obj['this']
|
||||
try:
|
||||
name,flags,desc = call_obj['stack'][:3]
|
||||
except ValueError:
|
||||
continue
|
||||
name=create_str(toAddr(name)).getValue()
|
||||
desc=create_str(toAddr(desc)).getValue()
|
||||
print(name,ref)
|
||||
ev_ns = make_namespace(["EngineVars"])
|
||||
ns = make_namespace(["EngineVars", name])
|
||||
clearListing(toAddr(engine_var))
|
||||
create_data(toAddr(engine_var),engine_var_dt).setComment(PLATE_COMMENT,desc)
|
||||
createLabel(toAddr(engine_var), name, ev_ns, True, SourceType.USER_DEFINED)
|
||||
clearListing(register_engine_var.getEntryPoint())
|
||||
createLabel(register_engine_var.getEntryPoint(), "register", ns, True, SourceType.USER_DEFINED)
|
||||
|
||||
# listing = currentProgram.getListing()
|
||||
# codeUnit = listing.getCodeUnitAt(minAddress)
|
||||
# codeUnit.setComment(codeUnit.PLATE_COMMENT, "AddCommentToProgramScript - This is an added comment!")
|
||||
|
||||
|
||||
# dtm = currentProgram.getDataTypeManager()
|
||||
# dt_engine_var = dtm.getDataType("/EngineVar")
|
||||
# dt_engine_ptr = dtm.getPointer(dt_engine_var)
|
125
tools/ghidra_scripts/mark_up_py.py
Normal file
125
tools/ghidra_scripts/mark_up_py.py
Normal file
|
@ -0,0 +1,125 @@
|
|||
import time
|
||||
try:
|
||||
import ghidra_bridge
|
||||
has_bridge=True
|
||||
except ImportError:
|
||||
has_bridge=False
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
if has_bridge:
|
||||
import ghidra_bridge
|
||||
b = ghidra_bridge.GhidraBridge(namespace=globals(), hook_import=True)
|
||||
@contextmanager
|
||||
def transaction():
|
||||
start()
|
||||
try:
|
||||
yield
|
||||
except Exception as e:
|
||||
end(False)
|
||||
raise e
|
||||
end(True)
|
||||
else:
|
||||
@contextmanager
|
||||
def transaction():
|
||||
yield
|
||||
|
||||
import ghidra.program.model.symbol.SymbolType as SymbolType
|
||||
import ghidra.program.model.symbol.SourceType as SourceType
|
||||
from ghidra.app.cmd.label import CreateNamespacesCmd
|
||||
from ghidra.program.model.data.DataUtilities import createData
|
||||
from ghidra.program.model.data.DataUtilities import ClearDataMode
|
||||
from ghidra.program.model.listing.CodeUnit import PLATE_COMMENT
|
||||
|
||||
listing = currentProgram.getListing()
|
||||
dtm = currentProgram.getDataTypeManager()
|
||||
py_mod = dtm.getDataType("/PyModuleDef")
|
||||
py_meth = dtm.getDataType("/PyMethodDef")
|
||||
|
||||
NULL=toAddr(0)
|
||||
|
||||
def make_namespace(parts):
|
||||
ns_cmd = CreateNamespacesCmd("::".join(parts), SourceType.USER_DEFINED)
|
||||
ns_cmd.applyTo(currentProgram)
|
||||
return ns_cmd.getNamespace()
|
||||
|
||||
def create_data(addr,dtype):
|
||||
return createData(currentProgram,addr,dtype,0,False,ClearDataMode.CLEAR_ALL_CONFLICT_DATA)
|
||||
|
||||
def create_str(addr):
|
||||
if addr.equals(NULL):
|
||||
return None
|
||||
str_len = (findBytes(addr, b"\0").offset - addr.offset) + 1
|
||||
clearListing(addr, addr.add(str_len))
|
||||
return createAsciiString(addr)
|
||||
|
||||
def get_call_obj(addr):
|
||||
func = getFunctionContaining(addr)
|
||||
if func is None:
|
||||
disassemble(addr)
|
||||
func = createFunction(addr,None)
|
||||
call_obj = {"this": None, "stack": []}
|
||||
for inst in currentProgram.listing.getInstructions(func.body, True):
|
||||
affected_objs = [r.toString() for r in inst.resultObjects.tolist()]
|
||||
inst_name = inst.getMnemonicString()
|
||||
if inst_name == "PUSH":
|
||||
val=inst.getScalar(0)
|
||||
if val is not None:
|
||||
call_obj["stack"].insert(0, toAddr(val.getValue()).toString())
|
||||
elif inst_name == "MOV" and "ECX" in affected_objs:
|
||||
this = inst.getScalar(1)
|
||||
if this is not None:
|
||||
call_obj["this"] = toAddr(this.getValue()).toString()
|
||||
elif inst_name == "CALL":
|
||||
break
|
||||
func=func.symbol.address
|
||||
return func, call_obj
|
||||
|
||||
def data_to_dict(data):
|
||||
ret={}
|
||||
for idx in range(data.dataType.getNumComponents()):
|
||||
name=data.dataType.getComponent(idx).getFieldName()
|
||||
value=data.getComponent(idx).getValue()
|
||||
ret[name]=value
|
||||
return ret
|
||||
|
||||
def try_create_str(addr):
|
||||
ret=create_str(addr)
|
||||
if ret:
|
||||
return ret.getValue()
|
||||
|
||||
with transaction():
|
||||
PyInitModule=getSymbolAt(toAddr("006f31c0"))
|
||||
for ref in getReferencesTo(PyInitModule.address).tolist():
|
||||
func,args=get_call_obj(ref.fromAddress)
|
||||
print(func,args)
|
||||
module_name=create_str(toAddr(args['stack'][0])).getValue()
|
||||
methods=toAddr(args['stack'][1])
|
||||
module_doc=create_str(toAddr(args['stack'][2]))
|
||||
if module_doc:
|
||||
module_doc=module_doc.getValue()
|
||||
print(methods,module_name,module_doc)
|
||||
mod_ns = make_namespace(["Python", module_name])
|
||||
createLabel(func, "__init__", mod_ns, True, SourceType.USER_DEFINED)
|
||||
if module_doc:
|
||||
listing.getCodeUnitAt(func).setComment(PLATE_COMMENT,module_doc)
|
||||
while True:
|
||||
mod_data=data_to_dict(create_data(methods,py_meth))
|
||||
if mod_data['name'] is None:
|
||||
clearListing(methods, methods.add(16))
|
||||
break
|
||||
mod_data['name']=try_create_str(mod_data['name'])
|
||||
try:
|
||||
mod_data['doc']=try_create_str(mod_data['doc'])
|
||||
except:
|
||||
mod_data['doc']=None
|
||||
print(mod_data)
|
||||
createLabel(mod_data['ml_method'], mod_data['name'], mod_ns, True, SourceType.USER_DEFINED)
|
||||
if mod_data['doc']:
|
||||
listing.getCodeUnitAt(mod_data['ml_method']).setComment(PLATE_COMMENT,module_doc)
|
||||
methods=methods.add(16)
|
||||
try:
|
||||
if getBytes(methods,4).tolist()==[0,0,0,0]:
|
||||
break
|
||||
except:
|
||||
break
|
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"));
|
||||
}
|
177
tools/remaster/scrap_parse/.gitignore
vendored
Normal file
177
tools/remaster/scrap_parse/.gitignore
vendored
Normal file
|
@ -0,0 +1,177 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
*.pkl.gz
|
1275
tools/remaster/scrap_parse/Cargo.lock
generated
Normal file
1275
tools/remaster/scrap_parse/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
26
tools/remaster/scrap_parse/Cargo.toml
Normal file
26
tools/remaster/scrap_parse/Cargo.toml
Normal file
|
@ -0,0 +1,26 @@
|
|||
[package]
|
||||
name = "scrap_parse"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.69"
|
||||
binrw = "0.11.1"
|
||||
chrono = { version = "0.4.23", features = ["serde"] }
|
||||
# chrono-humanize = "0.2.2"
|
||||
clap = { version = "4.1.6", features = ["derive"] }
|
||||
configparser = { version = "3.0.2", features = ["indexmap"] }
|
||||
flate2 = "1.0.25"
|
||||
fs-err = "2.9.0"
|
||||
indexmap = { version = "1.9.2", features = ["serde"] }
|
||||
# memmap2 = "0.5.10"
|
||||
modular-bitfield = "0.11.2"
|
||||
rhexdump = "0.1.1"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
serde-pickle = "1.1.1"
|
||||
serde_json = { version = "1.0.95", features = ["preserve_order", "unbounded_depth"] }
|
||||
steamlocate = "1.1.0"
|
||||
walkdir = "2.3.3"
|
||||
obj = "0.10.2"
|
23
tools/remaster/scrap_parse/blender_plugin/__init__.py
Normal file
23
tools/remaster/scrap_parse/blender_plugin/__init__.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
import pickle
|
||||
import subprocess as SP
|
||||
|
||||
from . import packed_browser
|
||||
from . import level_import
|
||||
|
||||
def scrap_bridge(*cmd):
|
||||
cmd=["scrap_parse",*cmd]
|
||||
proc=SP.Popen(cmd,stderr=None,stdin=None,stdout=SP.PIPE,shell=True,text=False)
|
||||
stdout,stderr=proc.communicate()
|
||||
code=proc.wait()
|
||||
if code:
|
||||
raise RuntimeError(str(stderr,"utf8"))
|
||||
return pickle.loads(stdout)
|
||||
|
||||
def register():
|
||||
packed_browser.register()
|
||||
level_import.regiser()
|
||||
|
||||
def unregister():
|
||||
packed_browser.unregister()
|
||||
level_import.unregister()
|
||||
|
510
tools/remaster/scrap_parse/blender_plugin/level_import.py
Normal file
510
tools/remaster/scrap_parse/blender_plugin/level_import.py
Normal file
|
@ -0,0 +1,510 @@
|
|||
import bpy
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import gzip
|
||||
import pickle
|
||||
import argparse
|
||||
from glob import glob
|
||||
from mathutils import Vector
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
import itertools as ITT
|
||||
from pprint import pprint
|
||||
# from .. import scrap_bridge
|
||||
import bmesh
|
||||
from bpy.props import StringProperty, BoolProperty
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from bpy.types import Operator
|
||||
|
||||
cmdline = None
|
||||
if "--" in sys.argv:
|
||||
args = sys.argv[sys.argv.index("--") + 1 :]
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--save", action="store_true")
|
||||
parser.add_argument("file_list", nargs="+")
|
||||
cmdline = parser.parse_args(args)
|
||||
|
||||
class ScrapImporter(object):
|
||||
def __init__(self, options):
|
||||
self.unhandled = set()
|
||||
filepath = options.pop("filepath")
|
||||
self.options = options
|
||||
self.model_scale = 1000.0
|
||||
self.spawn_pos = {}
|
||||
self.objects = {}
|
||||
# print("Loading", filepath)
|
||||
# scrapland_path=scrap_bridge("find-scrapland")
|
||||
# print(scrapland_path)
|
||||
# packed_data=scrap_bridge("parse-packed",scrapland_path)
|
||||
# print(packed_data)
|
||||
# get_output(["scrap_parse","parse-file","--stdout",scrapland_path,"levels/temple"])
|
||||
# raise NotImplementedError()
|
||||
with gzip.open(filepath, "rb") as fh:
|
||||
data = pickle.load(fh)
|
||||
self.path = data.pop("path")
|
||||
self.root = data.pop("root")
|
||||
self.config = data.pop("config")
|
||||
self.dummies = data.pop("dummies")["dummies"]
|
||||
self.moredummies = data.pop("moredummies")
|
||||
self.emi = data.pop("emi")
|
||||
self.sm3 = data.pop("sm3")
|
||||
|
||||
def make_empty(self, name, pos, rot=None):
|
||||
empty = bpy.data.objects.new(name, None)
|
||||
empty.empty_display_type = "PLAIN_AXES"
|
||||
empty.empty_display_size = 0.1
|
||||
empty.location = Vector(pos).xzy / self.model_scale
|
||||
if rot:
|
||||
empty.rotation_euler = Vector(rot).xzy
|
||||
empty.name = name
|
||||
empty.show_name = True
|
||||
bpy.context.scene.collection.objects.link(empty)
|
||||
|
||||
def create_tracks(self):
|
||||
points = {}
|
||||
for dummy in self.dummies:
|
||||
if dummy["name"].startswith("DM_Track"):
|
||||
try:
|
||||
_, name, idx = dummy["name"].split("_")
|
||||
except ValueError:
|
||||
continue
|
||||
pos = Vector(dummy["pos"]).xzy / self.model_scale
|
||||
points.setdefault(name, []).append((int(idx), pos))
|
||||
self.dummies=[d for d in self.dummies if not d["name"].startswith("DM_Track")]
|
||||
for name, points in points.items():
|
||||
crv = bpy.data.curves.new(name, "CURVE")
|
||||
crv.dimensions = "3D"
|
||||
crv.path_duration = (
|
||||
(bpy.context.scene.frame_end - bpy.context.scene.frame_start) + 1
|
||||
)
|
||||
crv.twist_mode = "Z_UP"
|
||||
crv.twist_smooth = 1.0
|
||||
spline = crv.splines.new(type="NURBS")
|
||||
spline.points.add(len(points) - 1)
|
||||
spline.use_endpoint_u = True
|
||||
spline.use_cyclic_u = True
|
||||
spline.use_endpoint_v = True
|
||||
spline.use_cyclic_v = True
|
||||
points.sort()
|
||||
for p, (_, co) in zip(spline.points, points):
|
||||
p.co = list(co) + [1.0]
|
||||
obj = bpy.data.objects.new(name, crv)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
|
||||
def create_dummies(self):
|
||||
for dummy in self.dummies:
|
||||
self.make_empty(dummy["name"], dummy["pos"], dummy["rot"])
|
||||
if dummy["name"].startswith("DM_Player_Spawn"):
|
||||
self.spawn_pos[dummy["name"]] = dummy["pos"]
|
||||
for name, dummy in self.moredummies.items():
|
||||
if not "Pos" in dummy:
|
||||
continue
|
||||
pos = list(float(v) for v in dummy["Pos"])
|
||||
rot = [0, 0, 0]
|
||||
if "Rot" in dummy:
|
||||
rot = list(float(v) for v in dummy["Rot"])
|
||||
self.make_empty(name, pos, rot)
|
||||
|
||||
def add_light(self, name, node):
|
||||
light = bpy.data.lights.new(name, "POINT")
|
||||
light.energy = 100
|
||||
r = node["unk_10"][0] / 255 # *(node['unk_10'][3]/255)
|
||||
g = node["unk_10"][1] / 255 # *(node['unk_10'][3]/255)
|
||||
b = node["unk_10"][2] / 255 # *(node['unk_10'][3]/255)
|
||||
light.color = (r, g, b)
|
||||
light = bpy.data.objects.new(name, light)
|
||||
light.location = Vector(node["pos"]).xzy / self.model_scale
|
||||
light.rotation_euler = Vector(node["rot"]).xzy
|
||||
bpy.context.scene.collection.objects.link(light)
|
||||
|
||||
def create_nodes(self):
|
||||
for node in self.sm3["scene"].get("nodes",[]):
|
||||
node_name = node["name"]
|
||||
node = node.get("content", {})
|
||||
if not node:
|
||||
continue
|
||||
if node["type"] == "Camera":
|
||||
print(f"CAM:{node_name}")
|
||||
pprint(node)
|
||||
elif node["type"] == "Light":
|
||||
print(f"LIGHT:{node_name}")
|
||||
# self.add_light(node_name, node)
|
||||
|
||||
def run(self):
|
||||
self.import_emi()
|
||||
self.join_objects(self.options['merge_objects'])
|
||||
if self.options['create_tracks']:
|
||||
self.create_tracks()
|
||||
if self.options['create_dummies']:
|
||||
self.create_dummies()
|
||||
if self.options['create_nodes']:
|
||||
self.create_nodes()
|
||||
if self.unhandled:
|
||||
print("Unhandled textures:",self.unhandled)
|
||||
|
||||
def join_objects(self, do_merge=False):
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
ctx = {}
|
||||
for name, objs in self.objects.items():
|
||||
if len(objs) <= 1:
|
||||
continue
|
||||
ctx = {
|
||||
"active_object": objs[0],
|
||||
"object": objs[0],
|
||||
"selected_objects": objs,
|
||||
"selected_editable_objects": objs,
|
||||
}
|
||||
with bpy.context.temp_override(**ctx):
|
||||
if do_merge:
|
||||
bpy.ops.object.join()
|
||||
objs[0].name=name
|
||||
else:
|
||||
coll=bpy.data.collections.new(name)
|
||||
bpy.context.scene.collection.children.link(coll)
|
||||
for n,obj in enumerate(objs):
|
||||
obj.name=f"{name}_{n:04}"
|
||||
coll.objects.link(obj)
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
|
||||
def import_emi(self):
|
||||
mats = {0: None}
|
||||
maps = {0: None}
|
||||
for mat in self.emi["materials"]:
|
||||
mats[mat[0]] = mat[1]
|
||||
for tex_map in self.emi["maps"]:
|
||||
maps[tex_map["key"]] = tex_map["data"]
|
||||
for tri in self.emi["tri"]:
|
||||
name = tri["name"]
|
||||
if tri["data"]:
|
||||
tris = tri["data"]["tris"]
|
||||
for n, verts in enumerate(
|
||||
[tri["data"]["verts_1"], tri["data"]["verts_2"]], 1
|
||||
):
|
||||
if not (tris and verts):
|
||||
continue
|
||||
self.create_mesh(
|
||||
name=f"{name}_{n}",
|
||||
verts=verts,
|
||||
faces=tris,
|
||||
m_map=(tri["data"]["map_key"], maps[tri["data"]["map_key"]]),
|
||||
m_mat=(tri["data"]["mat_key"], mats[tri["data"]["mat_key"]]),
|
||||
)
|
||||
|
||||
def normalize_path(self, path):
|
||||
return path.replace("\\", os.sep).replace("/", os.sep)
|
||||
|
||||
def resolve_path(self, path):
|
||||
file_extensions = [".png", ".bmp", ".dds", ".tga", ".alpha.dds"]
|
||||
root_path = Path(self.normalize_path(self.root).lower())
|
||||
start_folder = Path(self.normalize_path(self.path).lower()).parent
|
||||
try:
|
||||
texture_path = Path(self.config["model"]["texturepath"] + "/")
|
||||
except KeyError:
|
||||
texture_path = None
|
||||
path = Path(path.replace("/", os.sep).lower())
|
||||
if texture_path:
|
||||
folders = ITT.chain(
|
||||
[start_folder],
|
||||
start_folder.parents,
|
||||
[texture_path],
|
||||
texture_path.parents,
|
||||
)
|
||||
else:
|
||||
folders = ITT.chain([start_folder], start_folder.parents)
|
||||
folders=list(folders)
|
||||
print(f"Looking for {path} in {folders}")
|
||||
for folder in folders:
|
||||
for suffix in file_extensions:
|
||||
for dds in [".", "dds"]:
|
||||
resolved_path = (
|
||||
root_path / folder / path.parent / dds / path.name
|
||||
).with_suffix(suffix)
|
||||
if resolved_path.exists():
|
||||
return str(resolved_path)
|
||||
print(f"Failed to resolve {path}")
|
||||
return None
|
||||
|
||||
def get_input(self, node, name, dtype):
|
||||
return list(filter(lambda i: (i.type, i.name) == (dtype, name), node.inputs))
|
||||
|
||||
|
||||
def build_material(self, mat_key, mat_def, map_def):
|
||||
mat_props = dict(m.groups() for m in re.finditer(r"\(\+(\w+)(?::(\w*))?\)",mat_key))
|
||||
for k,v in mat_props.items():
|
||||
mat_props[k]=v or True
|
||||
skip_names = ["entorno", "noise_trazado", "noise128", "pbasicometal"]
|
||||
overrides = {
|
||||
"zonaautoiluminada-a.dds" : {
|
||||
# "light"
|
||||
},
|
||||
"flecha.000.dds": {
|
||||
"shader": "hologram"
|
||||
},
|
||||
"mayor.000.dds": {
|
||||
"shader": "hologram"
|
||||
},
|
||||
}
|
||||
settings = {
|
||||
"Emission Strength": 10.0,
|
||||
"Specular": 0.0,
|
||||
"Roughness": 0.0,
|
||||
"Metallic": 0.0,
|
||||
}
|
||||
transparent_settings = {
|
||||
"Transmission": 1.0,
|
||||
"Transmission Roughness": 0.0,
|
||||
"IOR": 1.0,
|
||||
}
|
||||
glass_settings = {
|
||||
"Base Color": ( .8, .8, .8, 1.0),
|
||||
"Metallic": 0.2,
|
||||
"Roughness": 0.0,
|
||||
"Specular": 0.2,
|
||||
}
|
||||
tex_slot_map={
|
||||
"base": "Base Color",
|
||||
"metallic":"Metallic",
|
||||
"unk_1":None, # "Clearcoat" ? env map?
|
||||
"bump":"Normal",
|
||||
"glow":"Emission"
|
||||
}
|
||||
|
||||
mat = bpy.data.materials.new(mat_key)
|
||||
mat.use_nodes = True
|
||||
node_tree = mat.node_tree
|
||||
nodes = node_tree.nodes
|
||||
imgs = {}
|
||||
animated_textures={}
|
||||
is_transparent = True
|
||||
print(map_def)
|
||||
if map_def[0]:
|
||||
print("=== MAP[0]:",self.resolve_path(map_def[0]))
|
||||
if map_def[2]:
|
||||
print("=== MAP[2]:",self.resolve_path(map_def[2]))
|
||||
for slot,tex in mat_def["maps"].items():
|
||||
slot=tex_slot_map.get(slot)
|
||||
if (slot is None) and tex:
|
||||
self.unhandled.add(tex["texture"])
|
||||
print(f"Don't know what to do with {tex}")
|
||||
if not (tex and slot):
|
||||
continue
|
||||
tex_file = self.resolve_path(tex["texture"])
|
||||
if tex_file is None:
|
||||
continue
|
||||
tex_name = os.path.basename(tex_file)
|
||||
if ".000." in tex_name:
|
||||
animated_textures[slot]=len(glob(tex_file.replace(".000.",".*.")))
|
||||
mat_props.update(overrides.get(tex_name,{}))
|
||||
if any(
|
||||
tex_name.find(fragment) != -1
|
||||
for fragment in skip_names
|
||||
):
|
||||
continue
|
||||
else:
|
||||
is_transparent = False
|
||||
imgs[slot]=bpy.data.images.load(tex_file,check_existing=True)
|
||||
for n in nodes:
|
||||
nodes.remove(n)
|
||||
out = nodes.new("ShaderNodeOutputMaterial")
|
||||
out.name = "Output"
|
||||
shader = nodes.new("ShaderNodeBsdfPrincipled")
|
||||
is_transparent|=mat_props.get("shader")=="glass"
|
||||
is_transparent|=mat_props.get("transp") in {"premult","filter"}
|
||||
if is_transparent:
|
||||
settings.update(transparent_settings)
|
||||
if mat_props.get("shader")=="glass":
|
||||
settings.update(glass_settings)
|
||||
for name, value in settings.items():
|
||||
shader.inputs[name].default_value = value
|
||||
for socket,img in imgs.items():
|
||||
img_node = nodes.new("ShaderNodeTexImage")
|
||||
img_node.name = img.name
|
||||
img_node.image = img
|
||||
if socket in animated_textures:
|
||||
img.source="SEQUENCE"
|
||||
num_frames=animated_textures[socket]
|
||||
fps_div = 2 # TODO: read from .emi file
|
||||
drv=img_node.image_user.driver_add("frame_offset")
|
||||
drv.driver.type="SCRIPTED"
|
||||
drv.driver.expression=f"((frame/{fps_div})%{num_frames})-1"
|
||||
img_node.image_user.frame_duration=1
|
||||
img_node.image_user.use_cyclic=True
|
||||
img_node.image_user.use_auto_refresh=True
|
||||
tex_mix_node = nodes.new("ShaderNodeMixRGB")
|
||||
tex_mix_node.blend_type = "MULTIPLY"
|
||||
tex_mix_node.inputs["Fac"].default_value = 0.0
|
||||
node_tree.links.new(
|
||||
img_node.outputs["Color"], tex_mix_node.inputs["Color1"]
|
||||
)
|
||||
node_tree.links.new(
|
||||
img_node.outputs["Alpha"], tex_mix_node.inputs["Color2"]
|
||||
)
|
||||
imgs[socket] = tex_mix_node
|
||||
output_node = tex_mix_node.outputs["Color"]
|
||||
print(img.name, "->", socket)
|
||||
if socket == "Normal":
|
||||
normal_map = nodes.new("ShaderNodeNormalMap")
|
||||
node_tree.links.new(output_node, normal_map.inputs["Color"])
|
||||
output_node = normal_map.outputs["Normal"]
|
||||
normal_map.inputs["Strength"].default_value = 0.4
|
||||
node_tree.links.new(output_node, shader.inputs[socket])
|
||||
shader_out=shader.outputs["BSDF"]
|
||||
if mat_props.get("shader")=="hologram":
|
||||
mix_shader = nodes.new("ShaderNodeMixShader")
|
||||
transp_shader = nodes.new("ShaderNodeBsdfTransparent")
|
||||
mix_in_1 = self.get_input(mix_shader,"Shader","SHADER")[0]
|
||||
mix_in_2 = self.get_input(mix_shader,"Shader","SHADER")[1]
|
||||
node_tree.links.new(transp_shader.outputs["BSDF"], mix_in_1)
|
||||
node_tree.links.new(shader.outputs["BSDF"], mix_in_2)
|
||||
node_tree.links.new(imgs["Base Color"].outputs["Color"],mix_shader.inputs["Fac"])
|
||||
node_tree.links.new(imgs["Base Color"].outputs["Color"],shader.inputs["Emission"])
|
||||
shader.inputs["Emission Strength"].default_value=50.0
|
||||
shader_out=mix_shader.outputs["Shader"]
|
||||
if settings.get("Transmission",0.0)>0.0:
|
||||
light_path = nodes.new("ShaderNodeLightPath")
|
||||
mix_shader = nodes.new("ShaderNodeMixShader")
|
||||
transp_shader = nodes.new("ShaderNodeBsdfTransparent")
|
||||
mix_in_1 = self.get_input(mix_shader,"Shader","SHADER")[0]
|
||||
mix_in_2 = self.get_input(mix_shader,"Shader","SHADER")[1]
|
||||
node_tree.links.new(shader.outputs["BSDF"], mix_in_1)
|
||||
node_tree.links.new(transp_shader.outputs["BSDF"], mix_in_2)
|
||||
node_tree.links.new(light_path.outputs["Is Shadow Ray"], mix_shader.inputs["Fac"])
|
||||
if mat_props.get("transp")=="filter" or mat_props.get("shader")=="glass":
|
||||
node_tree.links.new(imgs["Base Color"].outputs["Color"],transp_shader.inputs["Color"])
|
||||
shader_out=mix_shader.outputs["Shader"]
|
||||
node_tree.links.new(shader_out, out.inputs["Surface"])
|
||||
# try:
|
||||
# bpy.ops.node.button()
|
||||
# except:
|
||||
# pass
|
||||
return mat
|
||||
|
||||
def apply_maps(self, ob, m_mat, m_map):
|
||||
mat_key, m_mat = m_mat
|
||||
map_key, m_map = m_map
|
||||
if mat_key == 0:
|
||||
return
|
||||
mat_name = m_mat.get("name", f"MAT:{mat_key:08X}")
|
||||
if mat_name not in bpy.data.materials:
|
||||
ob.active_material = self.build_material(mat_name, m_mat, m_map)
|
||||
else:
|
||||
ob.active_material = bpy.data.materials[mat_name]
|
||||
|
||||
def create_mesh(self, name, verts, faces, m_mat, m_map):
|
||||
if not verts["inner"]:
|
||||
return
|
||||
me = bpy.data.meshes.new(name)
|
||||
me.use_auto_smooth = True
|
||||
pos = np.array([Vector(v["xyz"]).xzy for v in verts["inner"]["data"]])
|
||||
pos /= self.model_scale
|
||||
me.from_pydata(pos, [], faces)
|
||||
normals = [v["normal"] for v in verts["inner"]["data"]]
|
||||
vcols = [v["diffuse"] for v in verts["inner"]["data"]]
|
||||
if all(normals):
|
||||
normals = np.array(normals)
|
||||
me.vertices.foreach_set("normal", normals.flatten())
|
||||
if not me.vertex_colors:
|
||||
me.vertex_colors.new()
|
||||
for (vcol, vert) in zip(vcols, me.vertex_colors[0].data):
|
||||
vert.color = [vcol["r"], vcol["g"], vcol["b"], vcol["a"]]
|
||||
uvlayers = {}
|
||||
tex = [f"tex_{n+1}" for n in range(8)]
|
||||
for face in me.polygons:
|
||||
for vert_idx, loop_idx in zip(face.vertices, face.loop_indices):
|
||||
vert = verts["inner"]["data"][vert_idx]
|
||||
for tex_name in tex:
|
||||
if not vert[tex_name]:
|
||||
continue
|
||||
if not tex_name in uvlayers:
|
||||
uvlayers[tex_name] = me.uv_layers.new(name=tex_name)
|
||||
u, v = vert[tex_name]
|
||||
uvlayers[tex_name].data[loop_idx].uv = (u, 1.0 - v)
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(me)
|
||||
if self.options['remove_dup_verts']:
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
|
||||
bm.to_mesh(me)
|
||||
me.update(calc_edges=True)
|
||||
bm.clear()
|
||||
for poly in me.polygons:
|
||||
poly.use_smooth = True
|
||||
ob = bpy.data.objects.new(name, me)
|
||||
self.apply_maps(ob, m_mat, m_map)
|
||||
bpy.context.scene.collection.objects.link(ob)
|
||||
self.objects.setdefault(name.split("(")[0], []).append(ob)
|
||||
return ob
|
||||
|
||||
|
||||
class Scrap_Load(Operator, ImportHelper):
|
||||
|
||||
bl_idname = "scrap_utils.import_pickle"
|
||||
bl_label = "Import Pickle"
|
||||
|
||||
filename_ext = ".pkl.gz"
|
||||
filter_glob: StringProperty(default="*.pkl.gz", options={"HIDDEN"})
|
||||
|
||||
create_dummies: BoolProperty(
|
||||
name="Import dummies",
|
||||
default=True
|
||||
)
|
||||
|
||||
create_nodes: BoolProperty(
|
||||
name="Import nodes (lights, cameras, etc)",
|
||||
default=True
|
||||
)
|
||||
|
||||
create_tracks: BoolProperty(
|
||||
name="Create track curves",
|
||||
default=True
|
||||
)
|
||||
|
||||
merge_objects: BoolProperty(
|
||||
name="Merge objects by name",
|
||||
default=False
|
||||
)
|
||||
|
||||
remove_dup_verts: BoolProperty(
|
||||
name="Remove overlapping vertices\nfor smoother meshes",
|
||||
default=True
|
||||
)
|
||||
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.preferences.addon_enable(module = "node_arrange")
|
||||
bpy.ops.outliner.orphans_purge(do_recursive=True)
|
||||
importer = ScrapImporter(self.as_keywords())
|
||||
importer.run()
|
||||
dg = bpy.context.evaluated_depsgraph_get()
|
||||
dg.update()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def register():
|
||||
bpy.utils.register_class(Scrap_Load)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(Scrap_Load)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if cmdline is None or not cmdline.file_list:
|
||||
register()
|
||||
bpy.ops.scrap_utils.import_pickle("INVOKE_DEFAULT")
|
||||
else:
|
||||
for file in cmdline.file_list:
|
||||
bpy.context.preferences.view.show_splash = False
|
||||
objs = bpy.data.objects
|
||||
for obj in objs.keys():
|
||||
objs.remove(objs[obj], do_unlink=True)
|
||||
cols=bpy.data.collections
|
||||
for col in cols:
|
||||
cols.remove(col)
|
||||
importer = ScrapImporter(file)
|
||||
importer.run()
|
||||
if cmdline.save:
|
||||
targetpath = Path(file).name.partition(".")[0] + ".blend"
|
||||
targetpath = os.path.abspath(targetpath)
|
||||
print("Saving", targetpath)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=targetpath)
|
118
tools/remaster/scrap_parse/blender_plugin/packed_browser.py
Normal file
118
tools/remaster/scrap_parse/blender_plugin/packed_browser.py
Normal file
|
@ -0,0 +1,118 @@
|
|||
import sys
|
||||
from .. import scrap_bridge
|
||||
from bpy.props import (StringProperty, BoolProperty, CollectionProperty,
|
||||
IntProperty)
|
||||
|
||||
bl_info = {
|
||||
"name": "Packed Archive File",
|
||||
"blender": (2, 71, 0),
|
||||
"location": "File > Import",
|
||||
"description": "Import data from Scrapland .packed Archive",
|
||||
"category": "Import-Export"}
|
||||
|
||||
|
||||
|
||||
|
||||
class ImportFilearchives(bpy.types.Operator):
|
||||
"""Import whole filearchives directory."""
|
||||
bl_idname = "import_scene.packed"
|
||||
bl_label = 'Import Scrapland .packed'
|
||||
|
||||
directory = StringProperty(name="'Scrapland' folder",
|
||||
subtype="DIR_PATH", options={'HIDDEN'})
|
||||
filter_folder = BoolProperty(default=True, options={'HIDDEN'})
|
||||
filter_glob = StringProperty(default="", options={'HIDDEN'})
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def execute(self, context):
|
||||
# TODO: Validate filepath
|
||||
bpy.ops.ui.packed_browser('INVOKE_DEFAULT',filepath=self.directory)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class PackedFile(bpy.types.PropertyGroup):
|
||||
path = bpy.props.StringProperty()
|
||||
packed_file = bpy.props.StringProperty()
|
||||
selected = bpy.props.BoolProperty(name="")
|
||||
offset = bpy.props.IntProperty()
|
||||
size = bpy.props.IntProperty()
|
||||
|
||||
|
||||
archive = None
|
||||
class PackedBrowser(bpy.types.Operator):
|
||||
bl_idname = "ui.packed_browser"
|
||||
bl_label = "Packed Browser"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
files = CollectionProperty(type=PackedFile)
|
||||
selected_index = IntProperty(default=0)
|
||||
|
||||
def invoke(self, context, event):
|
||||
scrapland_path=scrap_bridge("find-scrapland")
|
||||
print(scrapland_path)
|
||||
packed_data=scrap_bridge("parse-packed",scrapland_path)
|
||||
print(packed_data)
|
||||
self.packed_data=packed_data
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
if self.selected_index != -1:
|
||||
print("new selected_index: " + str(self.selected_index))
|
||||
self.files.clear()
|
||||
for packed_name,files in self.archive:
|
||||
for file in files:
|
||||
entry = self.files.add()
|
||||
entry.packed_file = packed_name
|
||||
[entry.path,entry.offset,entry.size]=file
|
||||
self.selected_index = -1
|
||||
self.layout.template_list("PackedDirList", "", self, "current_dir", self, "selected_index")
|
||||
|
||||
def execute(self, context):
|
||||
print("execute")
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class PackedDirList(bpy.types.UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
operator = data
|
||||
packed_entry = item
|
||||
|
||||
if self.layout_type in {'DEFAULT', 'COMPACT'}:
|
||||
layout.prop(packed_entry, "name", text="", emboss=False, icon_value=icon)
|
||||
layout.prop(packed_entry, "selected")
|
||||
elif self.layout_type in {'GRID'}:
|
||||
layout.alignment = 'CENTER'
|
||||
layout.label(text="", icon_value=icon)
|
||||
|
||||
|
||||
def menu_func_import(self, context):
|
||||
self.layout.operator(ImportFilearchives.bl_idname, text="Scrapland .packed")
|
||||
|
||||
classes=[
|
||||
PackedFile,
|
||||
PackedDirList,
|
||||
PackedBrowser,
|
||||
ImportFilearchives,
|
||||
]
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.regiser_class(cls)
|
||||
bpy.types.INFO_MT_file_import.append(menu_func_import)
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
bpy.types.INFO_MT_file_import.remove(menu_func_import)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import imp
|
||||
imp.reload(sys.modules[__name__])
|
||||
for cls in classes:
|
||||
bpy.utils.regiser_class(cls)
|
||||
|
66
tools/remaster/scrap_parse/get_vertex_size.cpp
Normal file
66
tools/remaster/scrap_parse/get_vertex_size.cpp
Normal file
|
@ -0,0 +1,66 @@
|
|||
|
||||
int _D3DXGetFVFVertexSize(uint fvf)
|
||||
|
||||
{
|
||||
uint uVar1;
|
||||
uint uVar2;
|
||||
uint uVar3;
|
||||
int vert_size;
|
||||
|
||||
uVar1 = fvf & 0xe;
|
||||
vert_size = 0;
|
||||
if (uVar1 == 2) {
|
||||
vert_size = 0xc;
|
||||
}
|
||||
else if ((uVar1 == 4) || (uVar1 == 6)) {
|
||||
vert_size = 0x10;
|
||||
}
|
||||
else if (uVar1 == 8) {
|
||||
vert_size = 0x14;
|
||||
}
|
||||
else if (uVar1 == 0xa) {
|
||||
vert_size = 0x18;
|
||||
}
|
||||
else if (uVar1 == 0xc) {
|
||||
vert_size = 0x1c;
|
||||
}
|
||||
else if (uVar1 == 0xe) {
|
||||
vert_size = 0x20;
|
||||
}
|
||||
if ((fvf & 0x10) != 0) {
|
||||
vert_size += 0xc;
|
||||
}
|
||||
if ((fvf & 0x20) != 0) {
|
||||
vert_size += 4;
|
||||
}
|
||||
if ((fvf & 0x40) != 0) {
|
||||
vert_size += 4;
|
||||
}
|
||||
if (fvf < '\0') {
|
||||
vert_size += 4;
|
||||
}
|
||||
uVar1 = fvf >> 8 & 0xf;
|
||||
uVar3 = fvf >> 16;
|
||||
if (uVar3 == 0) {
|
||||
vert_size += uVar1 * 8;
|
||||
}
|
||||
else {
|
||||
for (; uVar1 != 0; uVar1 -= 1) {
|
||||
uVar2 = uVar3 & 3;
|
||||
if (uVar2 == 0) {
|
||||
vert_size += 8;
|
||||
}
|
||||
else if (uVar2 == 1) {
|
||||
vert_size += 0xc;
|
||||
}
|
||||
else if (uVar2 == 2) {
|
||||
vert_size += 0x10;
|
||||
}
|
||||
else if (uVar2 == 3) {
|
||||
vert_size += 4;
|
||||
}
|
||||
uVar3 >>= 2;
|
||||
}
|
||||
}
|
||||
return vert_size;
|
||||
}
|
15
tools/remaster/scrap_parse/src/find_scrap.rs
Normal file
15
tools/remaster/scrap_parse/src/find_scrap.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use steamlocate::SteamDir;
|
||||
use anyhow::{bail,Result};
|
||||
const APP_ID: u32 = 897610;
|
||||
|
||||
pub(crate) fn get_executable() -> Result<PathBuf> {
|
||||
let Some(mut steam) = SteamDir::locate() else {
|
||||
bail!("Failed to find steam folder");
|
||||
};
|
||||
let Some(app) = steam.app(&APP_ID) else {
|
||||
bail!("App {APP_ID} is not installed!");
|
||||
};
|
||||
Ok(app.path.clone())
|
||||
}
|
1068
tools/remaster/scrap_parse/src/main.rs
Normal file
1068
tools/remaster/scrap_parse/src/main.rs
Normal file
File diff suppressed because it is too large
Load diff
30
tools/remaster/scrap_parse/src/pixel_shader.rs
Normal file
30
tools/remaster/scrap_parse/src/pixel_shader.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
// https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx9-graphics-reference-asm-ps-1-x
|
||||
//
|
||||
// ################################################
|
||||
//
|
||||
// #[derive(Debug)]
|
||||
// enum VecArg {
|
||||
// Tex(f32,f32,f32,f32),
|
||||
// Reg(f32,f32,f32,f32),
|
||||
// Ver(f32,f32,f32,f32),
|
||||
// Col(f32,f32,f32,f32),
|
||||
// Vec(f32,f32,f32,f32),
|
||||
// }
|
||||
|
||||
// struct Arg {
|
||||
// arg: VecArg,
|
||||
// idx: Option<usize>
|
||||
// }
|
||||
|
||||
// #[derive(Debug)]
|
||||
// enum Inst {
|
||||
// Tex(Arg),
|
||||
// Add(Arg,Arg,Arag),
|
||||
// Sub(Arg,Arg,Arag),
|
||||
// Mul(Arg,Arg,Arag),
|
||||
// Mov(Arg,Arg),
|
||||
// }
|
||||
|
||||
fn parse(path: &str) {
|
||||
|
||||
}
|
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…
Reference in a new issue