Compare commits

..

6 commits

62 changed files with 11051 additions and 0 deletions

24
scrapper_web/.gitignore vendored Normal file
View 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
View file

@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

47
scrapper_web/README.md Normal file
View 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
View 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>

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

File diff suppressed because it is too large Load diff

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

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

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

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

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

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

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

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

@ -0,0 +1,6 @@
import './app.pcss'
import App from './App.svelte'
export default new App({
target: document.getElementById('app'),
});

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

@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

View 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(),
}

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

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

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

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

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

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
[package]
name = "scrap_net"
version = "0.1.0"
edition = "2021"
authors = ["Daniel Seiller <earthnuker@gmail.com>"]
description = "Scrapland Remastered network sniffer, proxy (and soon hopefully parser)"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chacha20 = { version = "0.9", features = ["std"] }
poly1305 = { version = "0.8", features = ["std"] }
rhexdump = "0.1"
tokio = { version = "1.21", features = ["full"] }
clap = {version = "4.0", features = ["derive"]}
rand = "0.8"
dialoguer = "0.10"
binrw = "0.11"
modular-bitfield = "0.11"
hex = "0.4"
lazy_static = "1.4.0"
rustyline-async = "0.3"
futures-util = "0.3.24"
itertools = "0.10.5"
anyhow = "1.0.68"
[profile.release]
lto="fat"
opt-level = 3

View file

@ -0,0 +1,22 @@
from distutils.command.install_data import install_data
import winreg as reg
import vdf
from pathlib import Path
import pefile
app_id="897610"
try:
key = reg.OpenKey(reg.HKEY_LOCAL_MACHINE,"SOFTWARE\\Valve\\Steam")
except FileNotFoundError:
key = reg.OpenKey(reg.HKEY_LOCAL_MACHINE,"SOFTWARE\\Wow6432Node\\Valve\\Steam")
path=Path(reg.QueryValueEx(key,"InstallPath")[0])
libraryfolders=vdf.load((path/"steamapps"/"libraryfolders.vdf").open("r"))['libraryfolders']
for folder in libraryfolders.values():
path=Path(folder['path'])
if app_id in folder['apps']:
install_dir = vdf.load((path/"steamapps"/f"appmanifest_{app_id}.acf").open("r"))['AppState']['installdir']
install_dir=path/"steamapps"/"common"/install_dir
for file in install_dir.glob("**/*.exe"):
pe = pefile.PE(file, fast_load=True)
entry = pe.OPTIONAL_HEADER.AddressOfEntryPoint
if pe.get_dword_at_rva(entry) == 0xE8:
print(file)

View file

@ -0,0 +1,93 @@
use itertools::Itertools;
use std::fmt::Display;
use std::ops::{Deref, DerefMut};
#[derive(Debug, PartialEq, Eq)]
enum HexII {
Ascii(char),
Byte(u8),
Null,
Full,
Eof,
}
impl From<&u8> for HexII {
fn from(v: &u8) -> Self {
match v {
0x00 => Self::Null,
0xFF => Self::Full,
c if c.is_ascii_graphic() => Self::Ascii(*c as char),
v => Self::Byte(*v),
}
}
}
impl Display for HexII {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HexII::Ascii(v) => write!(f, ".{}", v)?,
HexII::Byte(v) => write!(f, "{:02x}", v)?,
HexII::Null => write!(f, " ")?,
HexII::Full => write!(f, "##")?,
HexII::Eof => write!(f, " ]")?,
}
Ok(())
}
}
struct HexIILine(Vec<HexII>);
impl Display for HexIILine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (i, v) in self.0.iter().enumerate() {
if i != 0 {
write!(f, " ")?;
}
write!(f, "{}", v)?;
}
Ok(())
}
}
impl From<&[u8]> for HexIILine {
fn from(l: &[u8]) -> Self {
Self(l.iter().map(HexII::from).collect())
}
}
impl Deref for HexIILine {
type Target = Vec<HexII>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for HexIILine {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
pub fn hex_ii_dump<T: Iterator<Item = u8>>(data: T, base_offset: usize, total: usize) {
const CHUNK_SIZE: usize = 0x10;
let mut num_digits = (std::mem::size_of_val(&total) * 8) - (total.leading_zeros() as usize);
if (num_digits % 8) != 0 {
num_digits += 8 - (num_digits % 8)
}
num_digits >>= 2;
for (mut offset, line) in data.chunks(CHUNK_SIZE).into_iter().enumerate() {
offset += base_offset;
let mut line = HexIILine::from(line.collect::<Vec<_>>().as_slice());
if line.len() < CHUNK_SIZE {
line.push(HexII::Eof);
}
while line.len() < CHUNK_SIZE {
line.push(HexII::Null);
}
if line.iter().all(|v| v == &HexII::Null) {
continue;
}
let offset = format!("{:digits$x}", offset * CHUNK_SIZE, digits = num_digits);
println!("{} | {:<16} |", offset, line);
}
}

View file

@ -0,0 +1,640 @@
use anyhow::{bail, ensure, Result};
use binrw::BinReaderExt;
use binrw::{BinRead, NullString};
use chacha20::cipher::KeyInit;
use chacha20::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
use chacha20::ChaCha20;
use clap::Parser;
use dialoguer::theme::ColorfulTheme;
use dialoguer::Select;
use futures_util::FutureExt;
use poly1305::Poly1305;
use rand::{thread_rng, Rng};
use rhexdump::hexdump;
use rustyline_async::{Readline, ReadlineError, SharedWriter};
use std::collections::BTreeMap;
use std::error::Error;
use std::fmt::Display;
use std::io::Cursor;
use std::io::Write;
use std::iter;
use std::net::SocketAddr;
use std::net::ToSocketAddrs;
use std::net::{IpAddr, Ipv4Addr};
use std::path::PathBuf;
use std::time::{Duration, Instant};
use tokio::io::AsyncBufReadExt;
use tokio::net::UdpSocket;
use tokio::time;
mod hex_ii;
mod parser;
const KEY: &[u8; 32] = b"\x02\x04\x06\x08\x0a\x0c\x0e\x10\x12\x14\x16\x18\x1a\x1c\x1e\x20\x22\x24\x26\x28\x2a\x2c\x2e\x30\x32\x34\x36\x38\x3a\x3c\x3e\x40";
const INFO_PACKET: &[u8] = b"\x7f\x01\x00\x00\x07";
#[derive(Debug, Clone)]
struct ServerFlags {
dedicated: bool,
force_vehicle: bool,
_rest: u8,
}
impl Display for ServerFlags {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let force_vehicle = if self.force_vehicle { "F" } else { " " };
let dedicated = if self.dedicated { "D" } else { " " };
write!(f, "{}{}", force_vehicle, dedicated)?;
Ok(())
}
}
impl From<u8> for ServerFlags {
fn from(v: u8) -> Self {
ServerFlags {
dedicated: v & 0b1 != 0,
force_vehicle: v & 0b10 != 0,
_rest: (v & 0b11111100) >> 2,
}
}
}
#[derive(Debug, Clone, BinRead)]
#[br(little, magic = b"\xba\xce", import(rtt: Duration, addr: SocketAddr))]
pub struct Server {
#[br(calc=addr)]
addr: SocketAddr,
#[br(calc=rtt)]
rtt: Duration,
#[br(map = |v: (u8,u8)| format!("{}.{}",v.0,v.1))]
version: String,
port: u16,
max_players: u16,
cur_players: u16,
#[br(map = u8::into)]
flags: ServerFlags,
#[br(pad_size_to(0x20), map = |s :NullString| s.to_string())]
name: String,
#[br(pad_size_to(0x10), map = |s :NullString| s.to_string())]
mode: String,
#[br(pad_size_to(0x20), map = |s :NullString| s.to_string())]
map: String,
_pad: u8,
}
fn pad_copy(d: &[u8], l: usize) -> Vec<u8> {
let diff = d.len() % l;
if diff != 0 {
d.iter()
.copied()
.chain(iter::repeat(0).take(l - diff))
.collect()
} else {
d.to_vec()
}
}
fn pad(d: &mut Vec<u8>, l: usize) {
let diff = d.len() % l;
if diff != 0 {
d.extend(iter::repeat(0).take(l - diff))
}
}
struct Packet {
nonce: Vec<u8>,
data: Vec<u8>,
}
impl Packet {
fn encrypt(data: &[u8]) -> Packet {
let mut data: Vec<u8> = data.to_vec();
let mut rng = thread_rng();
let mut nonce = vec![0u8; 12];
rng.fill(nonce.as_mut_slice());
let mut cipher = ChaCha20::new(KEY.into(), nonce.as_slice().into());
cipher.seek(KEY.len() + 32);
cipher.apply_keystream(&mut data);
Packet { nonce, data }
}
fn get_tag(&self) -> Vec<u8> {
let mut sign_data = vec![];
sign_data.extend(pad_copy(&self.nonce, 16).iter());
sign_data.extend(pad_copy(&self.data, 16).iter());
sign_data.extend((self.nonce.len() as u64).to_le_bytes().iter());
sign_data.extend((self.data.len() as u64).to_le_bytes().iter());
let mut cipher = ChaCha20::new(KEY.into(), self.nonce.as_slice().into());
let mut poly_key = *KEY;
cipher.apply_keystream(&mut poly_key);
let signer = Poly1305::new(&poly_key.into());
signer.compute_unpadded(&sign_data).into_iter().collect()
}
fn bytes(&self) -> Vec<u8> {
let mut data = vec![];
data.extend(pad_copy(&self.nonce, 16).iter());
data.extend(pad_copy(&self.data, 16).iter());
data.extend((self.nonce.len() as u64).to_le_bytes().iter());
data.extend((self.data.len() as u64).to_le_bytes().iter());
data.extend(self.get_tag().iter());
data
}
fn decrypt(&self) -> Result<Vec<u8>> {
let mut data = self.data.clone();
let mut sign_data = data.clone();
pad(&mut sign_data, 16);
let mut nonce = self.nonce.clone();
pad(&mut nonce, 16);
let sign_data = nonce
.iter()
.chain(sign_data.iter())
.chain((self.nonce.len() as u64).to_le_bytes().iter())
.chain((self.data.len() as u64).to_le_bytes().iter())
.copied()
.collect::<Vec<u8>>();
let mut poly_key = *KEY;
let mut cipher = ChaCha20::new(KEY.into(), self.nonce.as_slice().into());
cipher.apply_keystream(&mut poly_key);
let signer = Poly1305::new(&poly_key.into());
let signature: Vec<u8> = signer.compute_unpadded(&sign_data).into_iter().collect();
if signature != self.get_tag() {
bail!("Invalid signature!");
};
cipher.seek(poly_key.len() + 32);
cipher.apply_keystream(&mut data);
Ok(data)
}
}
impl TryFrom<&[u8]> for Packet {
type Error = anyhow::Error;
fn try_from(data: &[u8]) -> Result<Self> {
let (mut nonce, data) = data.split_at(16);
let (mut data, tag) = data.split_at(data.len() - 16);
let nonce_len = u64::from_le_bytes(data[data.len() - 16..][..8].try_into()?) as usize;
let data_len = u64::from_le_bytes(data[data.len() - 8..].try_into()?) as usize;
data = &data[..data_len];
nonce = &nonce[..nonce_len];
let pkt = Packet {
nonce: nonce.into(),
data: data.into(),
};
if pkt.get_tag() != tag {
bail!("Invalid signature!");
}
Ok(pkt)
}
}
#[derive(Debug, Clone)]
pub enum ServerEntry {
Alive(Server),
Dead { addr: SocketAddr, reason: String },
}
impl Display for ServerEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ServerEntry::Alive(srv) => write!(
f,
"[{}] {} ({} {}/{} Players on {}) version {} [{}] RTT: {:?}",
srv.addr,
srv.name,
srv.mode,
srv.cur_players,
srv.max_players,
srv.map,
srv.version,
srv.flags,
srv.rtt
),
ServerEntry::Dead { addr, reason } => write!(f, "[{}] (error: {})", addr, reason),
}
}
}
fn encrypt(data: &[u8]) -> Vec<u8> {
Packet::encrypt(data).bytes()
}
fn decrypt(data: &[u8]) -> Result<Vec<u8>> {
Packet::try_from(data)?.decrypt()
}
async fn recv_from_timeout(
sock: &UdpSocket,
buf: &mut [u8],
timeout: f64,
) -> Result<(usize, SocketAddr)> {
Ok(time::timeout(Duration::from_secs_f64(timeout), sock.recv_from(buf)).await??)
}
async fn query_server<'a>(addr: SocketAddr) -> Result<Server> {
let mut buf = [0; 32 * 1024];
let socket = UdpSocket::bind("0.0.0.0:0").await?;
socket.connect(addr).await?;
let msg = encrypt(INFO_PACKET);
let t_start = Instant::now();
socket.send(&msg).await?;
let size = recv_from_timeout(&socket, &mut buf, 5.0).await?.0;
let rtt = t_start.elapsed();
let data = decrypt(&buf[..size])?;
if !data.starts_with(&[0xba, 0xce]) {
// Server Info
bail!("Invalid response");
}
let mut cur = Cursor::new(&data);
let info: Server = cur.read_le_args((rtt, addr))?;
if info.port != addr.port() {
eprint!("[WARN] Port differs for {}: {}", addr, info.port);
}
if cur.position() != (data.len() as u64) {
bail!("Leftover data");
}
Ok(info)
}
async fn get_servers(master_addr: &str) -> Result<(Duration, Vec<ServerEntry>)> {
let master_addr: SocketAddr = master_addr.to_socket_addrs()?.next().unwrap();
let mut rtt = std::time::Duration::from_secs_f32(0.0);
let mut servers = vec![];
let mut buf = [0; 32 * 1024];
let master = UdpSocket::bind("0.0.0.0:0").await?;
master.connect(master_addr).await?;
for n in 0..(256 / 32) {
let data = format!("Brw={},{}\0", n * 32, (n + 1) * 32);
let data = &encrypt(data.as_bytes());
let t_start = Instant::now();
master.send(data).await?;
let size = master.recv(&mut buf).await?;
rtt += t_start.elapsed();
let data = decrypt(&buf[..size])?;
if data.starts_with(b"\0\0\0\0}") {
for chunk in data[5..].chunks(6) {
if chunk.iter().all(|v| *v == 0) {
break;
}
let port = u16::from_le_bytes(chunk[chunk.len() - 2..].try_into()?);
let addr = SocketAddr::from(([chunk[0], chunk[1], chunk[2], chunk[3]], port));
let server = match query_server(addr).await {
Ok(server) => ServerEntry::Alive(server),
Err(err) => ServerEntry::Dead {
addr,
reason: err.to_string(),
},
};
servers.push(server);
}
}
}
rtt = Duration::from_secs_f64(rtt.as_secs_f64() / ((256 / 32) as f64));
Ok((rtt, servers))
}
fn indent_hexdump(data: &[u8], indentation: usize, label: &str) -> String {
let mut out = String::new();
let indent = " ".repeat(indentation);
out.push_str(&indent);
out.push_str(label);
out.push('\n');
for line in rhexdump::hexdump(data).lines() {
out.push_str(&indent);
out.push_str(line);
out.push('\n');
}
out.trim_end().to_owned()
}
#[derive(Default, Debug)]
struct State {
client: BTreeMap<usize, BTreeMap<u8, usize>>,
server: BTreeMap<usize, BTreeMap<u8, usize>>,
}
impl State {
fn update_client(&mut self, data: &[u8]) {
data.iter().enumerate().for_each(|(pos, b)| {
*self.client.entry(pos).or_default().entry(*b).or_default() += 1;
});
}
fn update_server(&mut self, data: &[u8]) {
data.iter().enumerate().for_each(|(pos, b)| {
*self.server.entry(pos).or_default().entry(*b).or_default() += 1;
});
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
enum Direction {
Client,
Server,
Both,
}
#[derive(Debug)]
enum CmdResult {
Exit,
Packet {
data: Vec<u8>,
direction: Direction,
},
Fuzz {
direction: Direction,
start: usize,
end: usize,
chance: (u32, u32),
},
NoFuzz,
Log(bool),
}
async fn handle_line(
line: &str,
state: &State,
stdout: &mut SharedWriter,
) -> Result<Option<CmdResult>> {
use CmdResult::*;
let cmd: Vec<&str> = line.trim().split_ascii_whitespace().collect();
match cmd[..] {
["log", "off"] => Ok(Some(Log(false))),
["log", "on"] => Ok(Some(Log(true))),
["state", pos] => {
let pos = pos.parse()?;
writeln!(stdout, "Client: {:?}", state.client.get(&pos))?;
writeln!(stdout, "Server: {:?}", state.server.get(&pos))?;
Ok(None)
}
[dir @ ("client" | "server"), ref args @ ..] => {
let mut data: Vec<u8> = vec![];
for args in args.iter() {
let args = hex::decode(args)?;
data.extend(args);
}
Ok(Some(CmdResult::Packet {
data,
direction: match dir {
"client" => Direction::Client,
"server" => Direction::Server,
_ => unreachable!(),
},
}))
}
["fuzz", dir @ ("client" | "server" | "both"), start, end, chance_num, chance_den] => {
let direction = match dir {
"client" => Direction::Client,
"server" => Direction::Server,
"both" => Direction::Both,
_ => unreachable!(),
};
let start = start.parse()?;
let end = end.parse()?;
if start > end {
bail!("Fuzz start>end");
}
let res = CmdResult::Fuzz {
direction,
start,
end,
chance: (chance_num.parse()?, chance_den.parse()?),
};
Ok(Some(res))
}
["fuzz", "off"] => Ok(Some(CmdResult::NoFuzz)),
["exit"] => Ok(Some(CmdResult::Exit)),
[""] => Ok(None),
_ => bail!("Unknown command: {:?}", line),
}
}
async fn run_proxy(
remote_addr: &SocketAddr,
local_addr: &SocketAddr,
logfile: &Option<PathBuf>,
) -> Result<()> {
let mut print_log = false;
let mut state = State::default();
let mut logfile = match logfile {
Some(path) => Some(std::fs::File::create(path)?),
None => None,
};
let mut fuzz = None;
let mut rng = thread_rng();
let mut client_addr: Option<SocketAddr> = None;
let local = UdpSocket::bind(local_addr).await?;
let remote = UdpSocket::bind("0.0.0.0:0").await?;
remote.connect(remote_addr).await?;
let mut local_buf = vec![0; 32 * 1024];
let mut remote_buf = vec![0; 32 * 1024];
println!("Proxy listening on {}", local_addr);
let (mut rl, mut stdout) = Readline::new(format!("{}> ", remote_addr)).unwrap();
loop {
tokio::select! {
line = rl.readline().fuse() => {
match line {
Ok(line) => {
let line=line.trim();
rl.add_history_entry(line.to_owned());
match handle_line(line, &state, &mut stdout).await {
Ok(Some(result)) => {
match result {
CmdResult::Packet{data,direction} => {
let data=encrypt(&data);
match direction {
Direction::Client => {
if client_addr.is_some() {
local
.send_to(&data, client_addr.unwrap())
.await?;
} else {
writeln!(stdout,"Error: No client address")?;
}
},
Direction::Server => {
remote.send(&data).await?;
}
Direction::Both => unreachable!()
};
}
CmdResult::Log(log) => {
print_log=log;
}
CmdResult::Exit => break Ok(()),
CmdResult::NoFuzz => {
fuzz=None;
}
CmdResult::Fuzz { .. } => {
fuzz=Some(result)
},
}
},
Ok(None) => (),
Err(msg) => {
writeln!(stdout, "Error: {}", msg)?;
}
}
},
Err(ReadlineError::Eof) =>{ writeln!(stdout, "Exiting...")?; break Ok(()) },
Err(ReadlineError::Interrupted) => {
writeln!(stdout, "^C")?;
break Ok(());
},
Err(err) => {
writeln!(stdout, "Received err: {:?}", err)?;
writeln!(stdout, "Exiting...")?;
break Ok(());
}
}
}
local_res = local.recv_from(&mut local_buf) => {
let (size, addr) = local_res?;
client_addr.get_or_insert(addr);
let mut data = Packet::try_from(&local_buf[..size])?.decrypt()?;
state.update_client(&data);
if print_log {
writeln!(stdout,"{}", indent_hexdump(&data, 0, &format!("OUT: {}", addr)))?;
}
if let Some(lf) = logfile.as_mut() {
writeln!(lf, ">{:?} {} {}", addr, data.len(), hex::encode(&data))?;
};
if let Some(CmdResult::Fuzz{direction,start,end,chance}) = fuzz {
if (direction==Direction::Server || direction==Direction::Both) && rng.gen_ratio(chance.0,chance.1) {
rng.fill(&mut data[start..end]);
}
}
remote.send(&encrypt(&data)).await?;
}
remote_res = remote.recv_from(&mut remote_buf) => {
let (size, addr) = remote_res?;
let mut data = Packet::try_from(&remote_buf[..size])?.decrypt()?;
state.update_server(&data);
if print_log {
writeln!(stdout,"\r{}", indent_hexdump(&data, 5, &format!("IN: {}", addr)))?;
}
if let Some(lf) = logfile.as_mut() {
writeln!(lf, "<{:?} {} {}", addr, data.len(), hex::encode(&data))?;
};
if client_addr.is_some() {
if let Some(CmdResult::Fuzz{direction,start,end,chance}) = &fuzz {
if (*direction==Direction::Client || *direction==Direction::Both) && rng.gen_ratio(chance.0,chance.1) {
rng.fill(&mut data[*start..*end]);
}
}
local
.send_to(&encrypt(&data), client_addr.unwrap())
.await?;
}
}
}
}
}
async fn send_master_cmd(sock: &UdpSocket, cmd: &str) -> Result<Vec<u8>> {
let mut buf = [0; 32 * 1024];
let mut data: Vec<u8> = cmd.as_bytes().to_vec();
data.push(0);
let data = &encrypt(&data);
sock.send(data).await?;
let size = recv_from_timeout(sock, &mut buf, 5.0).await?.0;
decrypt(&buf[..size])
}
async fn run_master_shell(master_addr: &str) -> Result<()> {
let master = UdpSocket::bind("0.0.0.0:0").await?;
master.connect(master_addr).await?;
let (mut rl, mut stdout) = Readline::new(format!("{}> ", master_addr)).unwrap();
loop {
tokio::select! {
line = rl.readline().fuse() => {
match line {
Ok(line) => {
let line=line.trim();
rl.add_history_entry(line.to_owned());
writeln!(stdout,"[CMD] {line}")?;
match send_master_cmd(&master,line).await {
Ok(data) => writeln!(stdout,"{}",hexdump(&data))?,
Err(e) => writeln!(stdout,"Error: {e}")?
}
}
Err(ReadlineError::Eof) =>{ writeln!(stdout, "Exiting...")?; break Ok(()) },
Err(ReadlineError::Interrupted) => {
writeln!(stdout, "^C")?;
break Ok(());
},
Err(err) => {
writeln!(stdout, "Receive error: {err}")?;
break Err(err.into());
}
}
}
}
}
}
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
/// Server to connect to (if unspecified will query the master server)
server: Option<SocketAddr>,
/// Only list servers without starting proxy
#[clap(short, long, action)]
list: bool,
/// Local Address to bind to
#[clap(short,long, default_value_t = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 28086))]
addr: SocketAddr,
/// Master server to query for running games
#[clap(short, long, default_value = "scrapland.mercurysteam.com:5000")]
master: String,
/// Path of file to log decrypted packets to
#[clap(short = 'f', long)]
logfile: Option<PathBuf>,
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
if args.list && args.server.is_some() {
let addr = args.server.unwrap();
let server = match query_server(addr).await {
Ok(server) => ServerEntry::Alive(server),
Err(msg) => ServerEntry::Dead {
addr,
reason: msg.to_string(),
},
};
println!("{}", server);
return Ok(());
}
if let Some(server) = args.server {
run_proxy(&server, &args.addr, &args.logfile).await?;
return Ok(());
}
loop {
let (rtt, servers) = get_servers(&args.master).await?;
println!("Master RTT: {:?}", rtt);
if args.list {
for server in servers {
println!("{}", server);
}
return Ok(());
}
let selection = Select::with_theme(&ColorfulTheme::default())
.items(&servers)
.with_prompt("Select server (press Esc to drop into master server command shell)")
.interact_opt()?
.map(|v| &servers[v]);
match selection {
Some(ServerEntry::Dead { addr, reason }) => {
eprintln!("{:?} returned an error: {}", addr, reason)
}
Some(ServerEntry::Alive(srv)) => {
return run_proxy(&srv.addr, &args.addr, &args.logfile).await;
}
None => {
return run_master_shell(&args.master).await;
}
}
}
}

View file

@ -0,0 +1,151 @@
use std::collections::HashMap;
use std::error::Error;
use crate::hex_ii::hex_ii_dump;
use crate::ServerFlags;
use binrw::BinReaderExt;
use binrw::{binread, BinRead, NullString};
/*
00000000: 7f 4c 00 00 06 ba ce 01 01 06 63 61 63 6f 74 61 | .L........cacota
00000010: 10 5b 42 4a 5a 5d 20 45 61 72 74 68 6e 75 6b 65 | .[BJZ].Earthnuke
00000020: 72 06 53 50 6f 6c 69 31 37 00 08 50 5f 50 6f 6c | r.SPoli17..P_Pol
00000030: 69 63 65 06 4d 50 4f 4c 49 31 00 00 00 0d 30 2c | ice.MPOLI1....0,
00000040: 30 2c 30 2c 31 2c 30 2c 30 2c 31 00 00 00 00 | 0,0,1,0,0,1....
00000000: 7f 49 00 00 06 ba ce 01 01 06 63 61 63 6f 74 61 | .I........cacota
00000010: 0e 55 6e 6e 61 6d 65 64 20 50 6c 61 79 65 72 07 | .Unnamed.Player.
00000020: 53 42 65 74 74 79 31 50 00 07 50 5f 42 65 74 74 | SBetty1P..P_Bett
00000030: 79 07 4d 42 65 74 74 79 31 00 00 00 0b 31 2c 31 | y.MBetty1....1,1
00000040: 2c 30 2c 31 2c 33 2c 30 00 00 00 00 | ,0,1,3,0....
*/
#[derive(Debug, Clone, BinRead)]
#[br(big)]
#[br(magic = b"\xba\xce")]
struct ServerInfoJoin {
#[br(map = |v: (u8,u8)| format!("{}.{}",v.0,v.1))]
version: String,
}
struct Data {
player_id: u32,
num_vals: u32,
pos: [f32; 3],
player_index: u32,
rtt: u32,
}
#[binread]
#[br(big)]
#[derive(Debug, Clone)]
enum PacketData {
#[br(magic = b"\x7f")]
PlayerJoin {
data_len: u8,
_1: u8,
cur_players: u8,
max_players: u8,
info: ServerInfoJoin,
#[br(temp)]
pw_len: u8,
#[br(count = pw_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
password: String,
#[br(temp)]
player_name_len: u8,
#[br(count = player_name_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
player_name: String,
#[br(temp)]
ship_model_len: u8,
#[br(count = ship_model_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
ship_model: String,
#[br(little)]
max_health: u16,
#[br(temp)]
pilot_model_len: u8,
#[br(count = pilot_model_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
pilot_model: String,
#[br(temp)]
engine_model_r_len: u8,
#[br(count = engine_model_r_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
engine_model_r: String,
#[br(temp)]
engine_model_l_len: u8,
#[br(count = engine_model_r_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
engine_model_l: String,
_2: u16,
#[br(temp)]
loadout_len: u8,
#[br(count = loadout_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
loadout: String,
team_number: u16,
padding: [u8; 2],
},
#[br(magic = b"\x80\x15")]
MapInfo {
#[br(temp)]
map_len: u32,
#[br(count = map_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
map: String,
#[br(temp)]
mode_len: u8,
#[br(count = mode_len, map = |bytes: Vec<u8>| String::from_utf8_lossy(&bytes).into_owned())]
mode: String,
_2: u16,
item_count: u8,
// _3: u32,
// #[br(count = item_count)]
// items: Vec<[u8;0x11]>
},
#[br(magic = b"\xba\xce")]
ServerInfo {
#[br(map = |v: (u8,u8)| format!("{}.{}",v.1,v.0))]
version: String,
port: u16,
max_players: u16,
cur_players: u16,
#[br(map = u8::into)]
flags: ServerFlags,
#[br(pad_size_to(0x20), map=|s: NullString| s.to_string())]
name: String,
#[br(pad_size_to(0x10), map=|s: NullString| s.to_string())]
mode: String,
#[br(pad_size_to(0x20), map=|s: NullString| s.to_string())]
map: String,
_pad: u8,
},
}
fn parse(data: &[u8]) -> Result<(PacketData, Vec<u8>), Box<dyn Error>> {
use std::io::Cursor;
let mut rdr = Cursor::new(data);
let pkt: PacketData = rdr.read_le()?;
let rest = data[rdr.position() as usize..].to_vec();
println!("{}", rhexdump::hexdump(data));
Ok((pkt, rest))
}
#[test]
fn test_parser() {
let log = include_str!("../test_.log").lines();
let mut hm = HashMap::new();
for line in log {
let data = line.split_ascii_whitespace().nth(1).unwrap();
let data = hex::decode(data).unwrap();
*hm.entry(data[0..1].to_vec()).or_insert(0usize) += 1;
match parse(&data) {
Ok((pkt, rest)) => {
println!("{:#x?}", pkt);
}
Err(e) => (),
}
}
let mut hm: Vec<(_, _)> = hm.iter().collect();
hm.sort_by_key(|(_, v)| *v);
for (k, v) in hm {
let k = k.iter().map(|v| format!("{:02x}", v)).collect::<String>();
println!("{} {}", k, v);
}
// println!("{:#x?}",parse("8015000000094c6576656c732f465a08466c616748756e7400000100000000000000000000000000000000000004105feb0006003e1125f3bc1300000019007e9dfa0404d5f9003f00000000000000000000"));
// println!("{:#x?}",parse("8015000000094c6576656c732f465a08466c616748756e7400002000000000000000000000000000000000000004105feb0006003e1125f3bc1300000019007e9dfa0404d5f9003f000000000000000000001f020b0376a8e2475b6e5b467c1e99461e020903982d14c5ec79cb45b2ee96471d020e03b29dbc46caa433464a28a0c71c020603aa80514658b8ab458db025c71b020803ce492f4658b8ab4514d320c71a02070344532f4658b8ab4587cf16c7190205031b3a0d4658b8ab459eaf25c7180206030ac34c4669e1fd469891ca47170208032e8c2a4669e1fd465500cd4716020703a4952a4669e1fd461b02d247150205037b7c084669e1fd460f92ca4714020603da6b7ec714aa3746b77c5a4713020803c87c83c714aa3746305a5f47120207039a7b83c714aa3746bd5d694711020503bfbe87c714aa3746a67d5a4710020803c5c719474ad5d445a7b3d2c60f0206037c5522474ad5d4459a6edcc60e02070323ca19474ad5d4458dacbec60d020503d84311474ad5d445bb6cdcc60c020603a9b16b47d52d974602dd15470b020803f2236347d52d97467bba1a470a02070350266347d52d974608be24470902050305a05a47d52d9746f1dd1547080206031f4066c6384b9c46955bd345070208037e3b84c6384b9c466147fa4506020703c33684c6384b9c46e431254605020503574395c6384b9c461063d34504020603ba349bc77a60294640f387c103020803957b9fc77a602946658f994402020703677a9fc77a60294680006d45010205038cbda3c77a602946807880c1"));
}

177
tools/remaster/scrap_parse/.gitignore vendored Normal file
View 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

File diff suppressed because it is too large Load diff

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

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

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

View 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 &gt; 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)

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

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

File diff suppressed because it is too large Load diff

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

View file

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

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,45 @@
[package]
name = "scraphacks_rs"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[profile.release]
debug = 0
[lib]
crate-type = ["dylib"]
[features]
console=[]
[dependencies]
anyhow = "1.0.68"
comma = "1.0.0"
custom-print = "0.1.0"
derivative = "2.2.0"
detour3 = "0.1.0"
discord-sdk = "0.3.2"
futures = "0.3.25"
hex = "0.4.3"
iced-x86 = "1.18.0"
mlua = { version = "0.8.7", features = ["luajit", "vendored", "macros", "serialize", "mlua_derive"] }
nom = "7.1.3"
nom-greedyerror = "0.5.0"
nom-supreme = "0.8.0"
nom_locate = "4.1.0"
num-traits = "0.2.15"
once_cell = "1.17.0"
parse_int = "0.6.0"
pelite = "0.10.0"
region = "3.0.0"
rhexdump = "0.1.1"
rustc-hash = "1.1.0"
shadow-rs = "0.21.0"
struct_layout = "0.1.0"
tokio = "1.24.2"
viable = "0.2.0"
winsafe = { version = "0.0.15", features = ["kernel", "user", "dshow"] }
[build-dependencies]
shadow-rs = "0.21.0"

View file

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

View file

@ -0,0 +1,242 @@
{
"id": "Outskirts - 07:41:13",
"title": "Scrapland savegame",
"data": {
"CTFFinishPlayerLoosesTextCad": "Mission_CTFAgainstBankers_RemoteMessage3",
"CTFFinishPlayerWinsTextCad": "Mission_CTFAgainstBankers_RemoteMessage2",
"CTFStartTextCad": "Mission_CTFAgainstBankers_RemoteMessage1",
"CTFOnDeathSpawnTime": "5.0",
"CTFFriendProfile": "BankersCTFFriends",
"CTFEnemyProfile": "BankersCTFEnemies",
"CTFFriendHead": "Functionary",
"CTFFriendType": "Functionary",
"CTFEnemyType": "BankDirector",
"CTFNumFriends": "5",
"CTFNumEnemies": "5",
"CTFFlags": "5",
"CombatFriendProfile": "ArrangeBankersCombatFriends",
"CombatFriendType": "Police",
"CombatNumFriends": "4",
"CombatNumEnemies": "5",
"PlayerWinsCombat": "1",
"OnVictory": "import SaveGame; SaveGame.QWayPoint();import MissionsFuncs; MissionsFuncs.SetNextMission(\"Mission_BackFromMortalRace\", \"GamblinDen\");Scrap.SetSaveVar(\"Map\", \"Levels/GamblinDen\");import SaveGame; SaveGame.QLoad(\"Levels/GamblinDen\");Scrap.SetSaveVar('MapPress_BureauExists','1')",
"OnAbort": "import MissionsFuncs; MissionsFuncs.EndOfSuperDeal('SuperDeal_Faliure_SystemMessage1')",
"CombatFinishPlayerLoosesTextCad": "SuperDeal_First_RemoteMessage3",
"CombatFinishPlayerWinsTextCad": "SuperDeal_First_RemoteMessage2",
"CombatStartTextCad": "SuperDeal_First_RemoteMessage1",
"CombatEnemyProfile": "SuperDealFirstElite",
"CombatEnemyTypeHead": "CrazyGambler",
"CombatEnemyType": "BankDirector",
"CombatDeaths": "5",
"SuperDealType": "",
"IgorFirstContactMissionState": "TalkToMercenaries",
"Stats.MadHunter": "10",
"Stats.Nurse.Dazed": "2",
"Stats.BankMaster": "6758",
"Bank.Circuit.36": "0",
"Bank.Circuit.35": "0",
"Bank.Circuit.34": "0",
"Bank.Circuit.33": "0",
"Bank.Circuit.32": "0",
"Bank.Circuit.31": "0",
"Bank.Circuit.30": "0",
"Bank.Circuit.29": "0",
"Bank.Circuit.28": "0",
"Bank.Circuit.27": "0",
"Bank.Circuit.26": "0",
"Bank.Circuit.25": "0",
"Bank.Circuit.24": "0",
"Bank.Circuit.23": "0",
"Bank.Circuit.22": "0",
"Bank.Circuit.21": "0",
"Bank.Circuit.20": "0",
"Bank.Circuit.19": "0",
"Bank.Circuit.18": "0",
"Bank.Circuit.17": "0",
"Bank.Circuit.16": "0",
"Bank.Circuit.15": "0",
"Bank.Circuit.14": "0",
"Bank.Circuit.13": "0",
"Bank.Circuit.12": "0",
"Bank.Circuit.11": "0",
"Bank.Circuit.10": "0",
"Bank.Circuit.9": "0",
"Bank.Circuit.8": "0",
"Bank.Circuit.7": "0",
"Bank.Circuit.6": "0",
"Bank.Circuit.5": "0",
"Bank.Circuit.4": "0",
"Bank.Circuit.3": "0",
"Bank.Circuit.2": "0",
"Bank.Circuit.1": "0",
"Bank.Circuit.0": "0",
"Stats.Mosquito": "116",
"PoliceBossAtTownHall": "0",
"Stats.Parking": "18",
"Police.FicusDeath": "1",
"CostumeAtPolice": "0",
"Hangar.HangarShip10": "SLifeBoat<-<-<-<-<-0,0,0,0,0,0,1<-30<-0,0,0,0,0,0",
"Hangar.iHangarShips": "8",
"AutoSaveGameOnLoad": "0",
"GameAct": "3rdMurder",
"BankDebt": "0",
"Map": "Levels/Outskirts",
"Mission.Library": "Mission_DestroyBadDebtors",
"EnergyBarActive": "1",
"SpecialActionActive": "2",
"CrazyWing.Status": "1",
"Conversor.ActiveConversors": "1",
"Hangar.iHangarShip": "0",
"Player.NumLives": "100",
"Hangar.shipsToEditList": "['SPoli1', 'SPoli2', 'SPoli3', 'SPoli4', 'SPoli5', 'SPoliBoss1', 'SMerc1', 'SMerc2', 'SMerc3', 'SMayor1', 'SBanker1', 'SBankMaster1', 'SBishop1', 'SArchbishop1', 'SFunc1', 'SBerto1', 'SBetty1', 'SHump1', 'SBoss1', 'SPoli4']",
"Hangar.availableEnginesList": "['MPOLI4', 'MPOLI5', 'MPOLIBOSS1', 'MPOLI2', 'MBERTO1', 'MBETTY1', 'MPOLI1', 'MMERC1', 'MMERC2', 'MPOLI3', 'MMAYOR1', 'MFUNC1', 'MBANKER1', 'MBANKMASTER1', 'MBISHOP1', 'MARCHBISHOP1', 'MHUMP1', 'MBOSS1', 'MMERC3']",
"Hangar.availableWeaponsList": "['Vulcan', 'Devastator', 'Swarm', 'Inferno', 'Tesla', 'ATPC', 'Swarm', 'Devastator']",
"Hangar.availableUpgradesList": "[\"VulcanUpgrade1\", \"VulcanUpgrade2\", \"DevastatorUpgrade1\", \"DevastatorUpgrade2\", \"SwarmUpgrade1\", \"SwarmUpgrade2\", \"InfernoUpgrade1\", \"InfernoUpgrade2\", \"TeslaUpgrade1\", \"TeslaUpgrade2\", \"ATPCUpgrade1\", \"ATPCUpgrade2\"]",
"JackInTheBox.Active": "1",
"Parked.Police": "['SPOLI1', 'SPOLI2', 'SPOLI3', 'SPOLI4']",
"Parked.Mercs": "['SMERC1', 'SMERC2']",
"Parked.TownHall": "['SFUNC1']",
"Parked.Bank": "['SBANKER1']",
"Parked.Press": "['SBERTO1', 'SBETTY1', 'SHUMP1']",
"Parked.Temple": "['SBISHOP1']",
"PoliceBlueprints.Ships": "['SPoli2', 'SPoli3', 'SPoli5', 'SPoliBoss1']",
"PoliceBlueprints.Engines": "['MPoli2', 'MPoli3', 'MPoli4', 'MPoli5', 'MPoliBoss1']",
"PressBlueprints.Ships": "['SBerto1', 'SBetty1', 'SHump1']",
"PressBlueprints.Engines": "['MBerto1', 'MBetty1', 'MHump1']",
"MayorAtGambinDen": "0",
"PolicesAtGambinDen": "1",
"PoliceBossAtPolice": "0",
"CrazyGamblerAtGambinDen": "1",
"FunctionaryTwinsAtGambinDen": "1",
"BankersAtGambinDen": "0",
"BishopsAtGambinDen": "0",
"GameSkill": "0",
"CreateBertos": "0",
"MercsHelpDtritus": "0",
"RobotsControlledByBoss": "0",
"Player.InfiniteLives": "0",
"PrevMap": "Levels/GamblinDen",
"Spawn": "DM_Player_Spawn_GamblinDen",
"Char": "Dtritus",
"ComeFrom": "DoorElevator",
"AlarmActive": "0",
"AlarmStatus": "0.0",
"Money": "2147483647",
"Challenge": "1",
"Challenge.Type": "",
"Challenge.Foe": "",
"Challenge.NumEnemies": "",
"Challenge.Money": "0",
"Revenge.Type": "",
"Revenge.Foe": "",
"Revenge.NumEnemies": "",
"Revenge.LastWinner": "",
"Mission.Map": "Outskirts",
"BadReputation": "3",
"GameplayTime": "27673.5857997",
"LonelyMercInGamblinDen": "0",
"LonelyMercLairActive": "1",
"LonelyMercDataActive": "0",
"ComSatsMissionsMapsFinished": "[]",
"Conversor.AvailableChars": "['Police', 'Nurse', 'BankDirector', 'Desktop', 'Sentinel', 'Gear', 'Bishop', 'Messenger', 'Functionary', 'Betty', 'Berto', 'BankMaster', 'Dtritus']",
"Batteries": "5",
"AcBatteries": "2",
"PrimaryMissionDesc": "Ich muss den Bankdirektor besuchen. Er treibt gerade ausstehende Kreditzahlungen mit seinem Kampfraumschiff ein. Ich sehe mal, ob ich ihm helfen kann.",
"SecondaryMissionDesc": "",
"TakePhotoMsg": "0",
"Race.Num": "3",
"Race.FirstTime": "0",
"Race.Profile": "Pilot",
"Combat.FirstTime": "1",
"Combat.Profile": "Rookie",
"Traffic.AcShips": "[\"SPoli6\", \"SMerc1\",\"SMerc2\",\"SBanker1\"]",
"IsSecondMission": "0",
"CrazyDeal.1.Var": "Stats.Dtritus",
"CrazyDeal.1.Tgt": "5",
"CrazyDeal.2.Var": "Stats.Parking",
"CrazyDeal.2.Tgt": "3",
"CrazyDeal.3.Var": "Stats.Battery",
"CrazyDeal.3.Tgt": "5",
"SuperDeal.Map": "FZ",
"SuperDeal.Library": "SuperDeal_Second",
"SuperDeal.Type": "MortalRace",
"CrazyWing.List": "['Sentinel', 'Betty', 'CrazyGambler', 'Functionary', 'Bishop']",
"Journalist.Humphrey_Defaut": "NoPlace",
"Journalist.Betty_Defaut": "NoPlace",
"Journalist.Berto_Defaut": "Press",
"Conversor.FirstConversion": "0",
"Conversor.FirstPossession": "0",
"WindowsError": "0",
"Orbit.Decontaminated": "yes",
"MercFriends.MercenaryA_Smartie": "0",
"MercFriends.MercenaryC_Brutus": "0",
"MercFriends.MercenaryB_Dumber": "0",
"StdShipAIProfile": "Elite",
"usrShip.Ammo00": "1000.0",
"usrShip.Ammo01": "500.0",
"usrShip.Ammo02": "1500.0",
"usrShip.AcWeap": "6",
"Parking.Desolate": "0",
"Hangar.HangarShip1": "SBoss1<-MBOSS1<-MBOSS1<-<-<-15,15,15,15,15,15,1<-187<-1,8,6,9,11,3",
"Hangar.HangarShip2": "",
"Hangar.HangarShip3": "",
"Hangar.HangarShip4": "",
"Hangar.HangarShip5": "",
"Hangar.HangarShip6": "",
"Hangar.HangarShip7": "",
"Hangar.HangarShip8": "",
"Hangar.HangarShip9": "",
"Hangar.HangarShipAux": "SLifeBoat<-<-<-<-<-0,0,0,0,0,0,1<-50<-0,0,0,0,0,0",
"Hangar.DestroyedShips": "[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]",
"NewBluePrintAvaliable": "11",
"DebugSave": "1",
"OutMusicRelax": "141",
"MayorAtTownHall": "1",
"GamblinMusic": "1",
"OutMusicAction": "122",
"Stats.Traffic": "767",
"Stats.OutPolice": "110",
"MapPress_HiAlarm": "0",
"MapPress_BureauExists": "1",
"Stats.PossessBerto": "1",
"Stats.ConvertIntoDtritus": "1",
"Stats.WinHumphreyRace": "1",
"Stats.Possession": "57",
"Stats.Betty": "49",
"Stats.Killer": "119",
"Stats.Jump": "1",
"Stats.Jump.Police": "1",
"Stats.Bishop": "10",
"Stats.Battery": "0",
"Stats.Dtritus": "0",
"Stats.Race.Press": "1",
"Stats.TotalRaces.Press": "1",
"BankMasterAtBank": "1",
"DM_ExtraLife_00": "-60",
"DM_ExtraLife_01": "-60",
"DM_ExtraLife_02": "-60",
"DM_ExtraLife_03": "-60",
"DM_ExtraLife_04": "16294.7919922",
"Stats.TempleLife": "4",
"Mission_CatchTrurl_Data": "[]",
"Mission_CatchTrurl_MapsPos": "[]",
"Mission_CatchTrurl_NumMapsDropped": "0",
"Mission_CatchTrurl_NumMapsTaken": "0",
"GDB.BishopsMsg": "1",
"Stats.GDB": "6",
"LonelyMercActive": "0",
"Stats.Messenger": "2",
"MortalRaceRace": "[((71611.59375, 18231.6992188, 93232.796875), 422.337219238), ((45388.4140625, 14599.3476563, 79622.6640625), 400.984222412), ((25194.9804688, 18783.4863281, 59759.296875), 404.390136719), ((-433.664245605, 26340.1289063, 34561.0898438), 409.718261719), ((-38229.3671875, 26457.5292969, 679.068054199), 449.472442627), ((-107464.132813, 19331.875, 3288.328125), 528.452758789), ((-113911.117188, 14331.4462891, 40812.9414063), 558.054199219), ((-102532.132813, 11236.1474609, 75072.375), 630.567077637), ((-58177.6289063, 6282.20654297, 74209.3515625), 673.615478516), ((-24157.5449219, 7054.30419922, 43223.1679688), 630.510681152), ((33550.1445313, 15480.2402344, 41122.5820313), -55.4565696716), ((56201.6054688, 15587.5126953, 24649.8496094), 23.7488441467), ((26343.9511719, 22077.8789063, -32317.0292969), 90.5086135864), ((-13835.4755859, 26276.8730469, -31975.1582031), 145.932754517), ((-29244.3652344, 26745.4667969, -2544.81738281), -892.995666504), ((-23808.9570313, 27246.9980469, 32018.1816406), -819.383483887), ((21584.3066406, 29452.4667969, 41221.6171875), -822.313781738), ((54026.796875, 24611.7421875, 42694.0898438), -802.188171387), ((95732.015625, 16516.8085938, 36323.546875), -872.699890137), ((113450.46875, 12325.5195313, 77796.75), -969.003662109)]",
"MortalRaceWaypoints": "20",
"MortalRaceRacers": "['ArchBishop', 'BankDirector', 'Functionary', 'MercenaryA', 'MercenaryB']",
"MortalRaceRacersProfile": "MortalRace",
"MortalRaceFightingFreezeControlAim": "4",
"MortalRaceRespawnTime": "5.0",
"MortalRaceStartTextCad": "Mission_WinMortalRace_RemoteMessage1",
"MortalRaceFinishPlayerLoosesTextCad": "Mission_WinMortalRace_RemoteMessage2",
"MortalRaceFinishPlayerLoosesTextFoe": "Messenger",
"MortalRaceFinishPlayerWinsTextCad": "Mission_WinMortalRace_RemoteMessage3",
"MortalRaceFinishPlayerWinsTextFoe": "Messenger",
"MortalRaceAutoRestart": "1"
}
}

View file

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

View file

@ -0,0 +1,4 @@
impo
cargo b -r
cp D:/devel/Git_Repos/Scrapland-RE/tools/remaster/scraphacks_rs/target/i686-pc-windows-msvc/release/scraphacks_rs.dll E:/Games/Steam/steamapps/common/Scrapland/lib/ScrapHack.pyd
# x32dbg E:/Games/Steam/steamapps/common/Scrapland/Bin/Scrap.unpacked.exe "-debug:10 -console"

View file

@ -0,0 +1,16 @@
# Notes
## Snippets
Map name: `Scrap.GetLangStr("Station_" + Scrap.GetLevelPath()[7:])`
## Run
`steam://run/897610/`
## Signatures
- World pointer: `a3 *{'} e8 ? ? ? ? 6a 00 68 *"World initialized"`
- print: `6a0068 *{"Scrap engine"} 6a?e8 $'`
- console handler: `68 *{"import Viewer"} e8 $'`
- DirectX Device: ``

View file

@ -0,0 +1,34 @@
import subprocess as SP
import shutil as sh
import json
from pathlib import Path
import psutil
import os
import sys
os.environ['DISCORD_INSTANCE_ID']='1'
SP.check_call(["cargo","b","-r"])
info=[json.loads(line) for line in SP.check_output(["cargo","b", "-r" ,"-q","--message-format=json"]).splitlines()]
dll_path=None
for line in info:
if line.get('reason')=="compiler-artifact" and ("dylib" in line.get("target",{}).get("crate_types",[])):
dll_path=Path(line['filenames'][0])
sh.copy(dll_path,"E:/Games/Steam/steamapps/common/Scrapland/lib/ScrapHack.pyd")
if "--run" not in sys.argv[1:]:
exit(0)
os.startfile("steam://run/897610/")
pid=None
while pid is None:
for proc in psutil.process_iter():
try:
if proc.name()=="Scrap.exe":
pid=proc.pid
except:
pass
print(f"PID: {pid:x}")
if "--dbg" in sys.argv[1:]:
SP.run(["x32dbg","-p",str(pid)])
# cp D:/devel/Git_Repos/Scrapland-RE/tools/remaster/scraphacks_rs/target/i686-pc-windows-msvc/release/scraphacks_rs.dll E:/Games/Steam/steamapps/common/Scrapland/lib/ScrapHack.pyd
# x32dbg E:/Games/Steam/steamapps/common/Scrapland/Bin/Scrap.unpacked.exe "-debug:10 -console"

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

View file

@ -0,0 +1,7 @@
enum FilePatch {
}
pub struct Config {
file_patches: FxHashMap<PathBuf,FilePatch>
}

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

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

View 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()?)
}

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

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

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

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

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

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