Compare commits
11 Commits
5946e0910b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dabdf51383 | ||
|
|
ec1295e39c | ||
|
|
f1dbc06425 | ||
|
|
43cc032e02 | ||
|
|
e390461a53 | ||
|
|
f049db6921 | ||
|
|
608ea295a0 | ||
|
|
bd01d6753a | ||
|
|
aa7425221f | ||
|
|
fc6b270fa8 | ||
|
|
3df0797acc |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -43,3 +43,8 @@ dist/
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
/reverse/.secrets/
|
||||
|
||||
# Extracted game data + scratch output (regenerable; kept locally, not tracked)
|
||||
/extracted/
|
||||
/tmp/
|
||||
|
||||
170
CLAUDE.md
Normal file
170
CLAUDE.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What this repo is
|
||||
|
||||
Reverse-engineering / data-mining toolkit for the game **SAND** (Hologryph, Unity 6000.0.40f1,
|
||||
IL2CPP). It extracts server-authoritative and static game data from three sources, and reads/writes
|
||||
the game's local walker save files. There is no application to build — everything is standalone
|
||||
Python scripts run against game files, network captures, or live servers.
|
||||
|
||||
The four data sources, and which tools own them:
|
||||
- **Unity asset bundles** (static config: items, recipes, loot, islands) → `bundle/`
|
||||
- **Master server** `wss://<region>.hologryph.com/gameclient/` (economy, walker blueprints, research) → `reverse/master_scrape.py`
|
||||
- **PlayFab** (Azure; **auth-only** for this title — Economy/catalog disabled) → `reverse/playfab_scrape.py`
|
||||
- **`.wbt` walker save files** (local, on disk) → `walker/`
|
||||
|
||||
## Working rules (from operator memory — follow these)
|
||||
|
||||
- **Data only, never heuristics.** Do not invent rules or fill gaps with plausible assumptions.
|
||||
Derive every value from game files, decompiled code, or captured payloads — or ask. (An invented
|
||||
"guns point outward" rule and a guessed rotation→facing mapping both produced wrong results.)
|
||||
- **Report what the data shows, not inferences as fact.** Don't jump to conclusions.
|
||||
- **No polling wait-loops.** Use background tasks + wakeup notifications; don't `sleep`-poll for completion.
|
||||
- **Don't hammer the live server.** It is a real playtest backend. Warn the operator *before* any
|
||||
action that makes repeated/abnormal connections. BattlEye is active in the game — all scraping is
|
||||
done **outside** the game process (replayed protocol / captures / REST), never via injection.
|
||||
- **A `/connect` scrape kicks the live player** (single session per account, newest wins — verified
|
||||
2026-06-16, see `docs/MASTER_SERVER.md`). Don't open `/connect` while the operator is in-game.
|
||||
|
||||
## Environment & how to run
|
||||
|
||||
- Use the project venv: **`venv/bin/python <script>`** (has `UnityPy`, `bson`/pymongo, `scapy`, `Pillow`, `websockets`).
|
||||
- Symlinks (git-ignored, machine-specific — repoint if the machine changes):
|
||||
- `bundles/` → game `StreamingAssets/aa/StandaloneWindows64/` (35 bundles, ~6.8 GB)
|
||||
- `Walkers/` → `…/LocalLow/Hologryph/SAND/Data/Walkers/` (live `.wbt` saves)
|
||||
- Game install referenced by `bundle/` tools: `/mnt/d/SteamLibrary/steamapps/common/Sand Playtest`
|
||||
(`GameAssembly.dll` + `Sand_Data/il2cpp_data/Metadata/global-metadata.dat`).
|
||||
- IL2CPP source of truth: `il2cpp/dump.cs` (Il2CppDumper output — signatures/RVAs only, no method
|
||||
bodies). `il2cpp/`, `ghidra/`, `snapshots/`, `bundles`, `Walkers`, `reverse/.secrets/` are
|
||||
git-ignored (large/regenerable/secret). Live PlayFab token: `reverse/.secrets/playfab_token.json`.
|
||||
- **Game runtime data dir** (`%USERPROFILE%\AppData\LocalLow\Hologryph\SAND`, here
|
||||
`/mnt/c/Users/DownloadPizza/AppData/LocalLow/Hologryph/SAND/`) holds:
|
||||
- **`Player.log`** — Unity log; check it to see what the client did (walker file reads:
|
||||
`[FS_STANDALONE] ReadAllFilesAsync … Path: …/Data/Walkers`; master-server handshake:
|
||||
`[MasterServer] … Login / Connection failed to /connect`). `Player-prev.log` = previous run.
|
||||
- **`Data/Walkers/*.wbt`** — the live walker saves (the `Walkers/` symlink points here).
|
||||
|
||||
## The `.wbt` walker save format (current focus)
|
||||
|
||||
Envelope (RE'd from `XorCryptography.Encrypt`, verified byte-exact on all 5 local walkers):
|
||||
```
|
||||
save: Newtonsoft-BSON -> XOR encrypt -> gzip
|
||||
load: gunzip -> XOR decrypt -> Newtonsoft-BSON parse
|
||||
```
|
||||
- XOR key (current build): `70 DD 1F 2A 0B 4A` (6 bytes), applied **per 0xA000-byte chunk** with the
|
||||
key index reset to 0 at each chunk boundary: `decoded[i] = raw[i] XOR KEY[(i % 0xA000) % 6]`.
|
||||
If a game update changes the key, recover it with no RE via `walker/recover_key.py`.
|
||||
- `pymongo`'s `bson.encode` reproduces Newtonsoft.Bson byte-for-byte, so decode→encode is identity.
|
||||
|
||||
### Walker naming convention
|
||||
A walker's display name is **two indices**, not a string: top-level `firstNameIndex` + `secondNameIndex`
|
||||
(BSON int32, **0–31 each**). Resolve them via `name_index.json` → `first_name` / `second_name` tables
|
||||
(e.g. `(16,5)` = "Veteran Veteran", second `6` = "…Beast"). The top-level `name` field is **null/unused**
|
||||
— the shown name comes from the indices. "name1" = `firstNameIndex`, "name2" = `secondNameIndex`. Set
|
||||
both with `build_wbt.py rename <wbt> <first> <second> -o out`, or edit the index directly when copying.
|
||||
|
||||
### The 5 hashes (a `.wbt` is a serialized `WalkerBlueprintDto`)
|
||||
|
||||
All = `MD5(UTF8(JsonConvert.SerializeObject(obj))).hexUPPER` — Newtonsoft compact JSON: no whitespace,
|
||||
PascalCase, **member declaration order** (do NOT sort keys), nulls included, **enums as NAME strings**.
|
||||
|
||||
| Hash | Scope | Offline-computable? |
|
||||
|------|-------|---------------------|
|
||||
| `CompartmentsHash` | top-level: `MD5(JSON(Compartments list))` | **YES** — `walker/walker_hashes.py` |
|
||||
| `ConnectionsHash` | top-level: `MD5(JSON(Connections list))` | **YES** — `walker/walker_hashes.py` |
|
||||
| `CompartmentHash` | per-part: placement from CompartmentsDatabase | **YES** — constant per EpbId+placement |
|
||||
| `DefinitionsHash` | top-level: `MD5(JSON(Compartments→CompartmentDefinitionDto))` | **NO** — server-sourced |
|
||||
| `DefinitionHash` | per-part: `MD5(JSON(CompartmentDefinitionDto))` | **NO** — server-sourced |
|
||||
|
||||
The two **Definition** hashes hash the rich server-side `CompartmentDefinitionDto`, which is **not**
|
||||
in the blueprint and **not** equal to the server's `GetCompartmentDefinitions` pricing DTO. They can
|
||||
only be **harvested** (every part placed in-game writes them into the save) — see
|
||||
`extracted/definition_hashes_known.json` (~18/126 parts) and `walker/harvest_hashes.py`. When editing
|
||||
offline, `build_wbt.py pack` recomputes the two Compartment* hashes and **copies/reuses** the
|
||||
Definition* hashes from the source — correct as long as the *set of part definitions* is unchanged;
|
||||
it raises if you add a part whose `DefinitionHash` has never been harvested.
|
||||
|
||||
**Hash lifecycle (verified live 2026-06-16 — see `docs/TRAMPLER.md`):** the client **recomputes all 5
|
||||
hashes on save** from its own database (same build ⇒ byte-identical hashes; no per-walker secret —
|
||||
plain unsalted MD5, they're integrity/version-staleness markers, **not** security). Wiping any/all
|
||||
hashes is harmless: a walker with blank hashes still **loads, lists, and opens in the editor**, and
|
||||
**one in-editor save regenerates everything** (and mints a new file UUID + `UniqueId`). The `VERSION`
|
||||
flag in `Player.log`'s `CheckValidBlueprint: ERRORS {0}, VERSION:{1}` (=`WalkerBlueprintContainer
|
||||
.ValidateVersion`, which recomputes against the *current* DB/definitions) tracks only the **structural**
|
||||
hashes (Compartments/Connections); Definition hashes don't affect client validation. Server-side upsert
|
||||
validation is untested (the master-server `/connect` blocker is cleared as of 2026-06-16 — server back
|
||||
up — but the live re-test has not been run yet).
|
||||
|
||||
Enum tables (from `dump.cs`): `ConnectionSlotType` 0 DOOR,1 HATCH,2 STRUCTURE,3 BALCONY,4 DECK ·
|
||||
`ConnectionState` 0 DEFAULT,1 DOOR,2 OPEN · `ConnectionsCount` 0 FULL,1 PARTIAL,2 ERROR. Note the
|
||||
master-server **WS form** serializes these as integers and omits null `EpbId`; the storage/hash form
|
||||
uses name strings and includes `EpbId:null` — convert before hashing (`reverse/walkerdto_to_blueprint.py`).
|
||||
|
||||
## Tools (all scripts, with subcommands)
|
||||
|
||||
### `walker/` — `.wbt` save files (offline edit + hashes)
|
||||
- **`sand.py`** — low-level toolkit. Subcommands: `decode <wbt> [-o]` · `snap (--all | files…)` ·
|
||||
`diff <before> <after> [--no-filter]` · `check <wbt> [--no-filter]` · `watch <wbt> [--interval]`.
|
||||
- **`build_wbt.py`** — high-level edit/build. Subcommands: `repack <wbt>` (identity sanity) ·
|
||||
`rename <wbt> <first> <second> [-o]` (name indices 0–31) · `pack <wbt> -o out [--no-strict]`
|
||||
(recompute hashes, write fresh) · `get-icon <wbt> [-o png]` · `set-icon <wbt> <png> [-o]`.
|
||||
- **`harvest_hashes.py`** — scan saves+snapshots, merge `EpbId→{DefinitionHash,CompartmentHash}` into
|
||||
the known-hashes table. Usage: `harvest_hashes.py [extra_dir …]`.
|
||||
- **`recover_key.py`** — recover the XOR key from known-plaintext (the icon background pixel) after a
|
||||
game update; no RE needed. Usage: `recover_key.py <wbt> …`.
|
||||
- **`walker_hashes.py`** — reproduce `CompartmentsHash`/`ConnectionsHash` offline (the verified module).
|
||||
|
||||
### `bundle/` — Unity asset-bundle extraction (static data)
|
||||
All use UnityPy with an IL2CPP TypeTreeGenerator (`GameAssembly.dll` + `global-metadata.dat`).
|
||||
- **`unitybundle.py`** — minimal UnityFS extractor (LZ4/LZ4HC + uncompressed). `unitybundle.py [needle]`.
|
||||
- **`odin_read.py`** — Sirenix **Odin** Binary (SerializedFormat=0) reader; used to decode
|
||||
`SerializedBytes` blobs. `odin_read.py <file> [out]`.
|
||||
- **`extract_data.py`** — generic MonoBehaviour extractor via typetrees → JSON in `extracted/`.
|
||||
- **`extract_loot.py`** — loot/drop tables (Odin) → `extracted/loot_tables.json`.
|
||||
- **`extract_production_lines.py`** — world conveyor single-recipe production lines → `extracted/production_lines.json`.
|
||||
- **`extract_conveyor_placements.py`** — map islands→conveyors → `extracted/conveyor_placements.json`.
|
||||
- **`extract_island_names.py`** — prefab→in-game Toponym (via `LandmarkBehaviour`) → `extracted/island_names.json`.
|
||||
- **`extract_i2.py`** — I2 Localization English term table (manual parse) → i2 terms JSON.
|
||||
- **`workbench_bundles.py`** — workbench EntityBlueprint → referenced `CraftingRecipeBundle`s.
|
||||
- **`discord_recipes.py`** — emit Discord monospace recipe tables (workbench + production lines).
|
||||
- **`component_census.py`** — tally ECS `$type` components across all 1446 EntityBlueprints. `component_census.py [filter]`.
|
||||
- **`dump_blueprint.py`** — fully decode named EntityBlueprint(s): components + scalar fields. `dump_blueprint.py <base> …`.
|
||||
- **`dump_loot_bytes.py`** / **`loot_probe.py`** — raw Odin byte dump / locate loot configs (analysis helpers).
|
||||
|
||||
### `reverse/` — network scraping + IL2CPP RE
|
||||
- **`master_scrape.py`** — **the working master-server client** (2026-06-15 build). Two-socket
|
||||
ClientMessage handshake: `/login` (no header) → `/connect` (`Authorization: <server ticket>`).
|
||||
Flags: `--region {ger,eus,…}` `--go` (ARM network) `--data` `--user` `--client-version` `--insecure`
|
||||
`--selftest`. Does nothing over the network without `--go`. See `docs/MASTER_SERVER.md` for the full
|
||||
`ClientAction` enum / `OperationResult<T>` envelope.
|
||||
- **`playfab_scrape.py`** — PlayFab REST (read-only), runs outside the game. Required `--title-id`;
|
||||
auth via `--steam-ticket` or `--entity-token`; modes `--catalog` `--inventory` `--titledata`.
|
||||
(Note: catalog/economy is disabled for this title — PlayFab is effectively auth-only.)
|
||||
- **`capture_hosts.py`** — triage a pcap: DNS/SNI/endpoints, prints the PlayFab TitleId + master region. `capture_hosts.py <pcap>`.
|
||||
- **`noise_filter.py`** — baseline-subtract a "SAND-off" pcap from a session pcap to isolate game traffic. `noise_filter.py <baseline> [session]`.
|
||||
- **`ws_scrape.py`** — decode master-server WS frames from a pcap (older cleartext-era decoder; tries JSON/BSON/MessagePack). `ws_scrape.py <pcap> [--port --host --out]`.
|
||||
- **`trampler_hashes.py`** — generate the blueprint hashes from scratch (Definition hash provisional). Self-test: run directly.
|
||||
- **`walkerdto_to_blueprint.py`** — convert master-server `WalkerDto` (e.g. `GetExpedition.Trampler`) → loadable `WalkerBlueprintDto` + recompute hashes. Self-verifies via round-trip.
|
||||
- **`render_trampler.py`** — render a multi-floor PNG map of a trampler (footprints, doors/hatches, guns) → `extracted/host_trampler_*.png`.
|
||||
- **`il2cpp_re.py`** — IL2CPP helpers: VA↔file-offset, method index from `dump.cs`, xref finder, body disasm + float-constant extraction.
|
||||
- **`resolve_decomp.py`** — annotate `ghidra/decomp.c` with symbol names + string literals. `resolve_decomp.py [substr]`.
|
||||
- **`ghidra_decomp_targets.py`** / **`find_damage_writes.py`** — Ghidra headless decompile-target script / scan decomp for damage-write fingerprint.
|
||||
|
||||
### `wikigen/` — generate MediaWiki pages from `extracted/`
|
||||
`make_items_wiki.py` · `make_crafting_wiki.py` · `make_loot_wiki.py` (→ `wiki/*.mediawiki`) ·
|
||||
`render_wiki.py` (wikitext → standalone HTML in `wiki_site/`, git-ignored).
|
||||
|
||||
## Reference docs (`docs/`)
|
||||
|
||||
- **`MASTER_SERVER.md`** — master-server WebSocket protocol & scrape (transport, two-socket handshake, ClientAction enum, OperationResult).
|
||||
- **`BACKEND_PLAYFAB.md`** — PlayFab is auth-only; read the corrections block at top.
|
||||
- **`TRAMPLER.md`** — walker blueprint structure, the hashes, footprints, rendering.
|
||||
- **`TASK.md`** — `.wbt` format cracked (BSON-verified) summary.
|
||||
- **`PRODUCTION_LINES.md`**, **`SALES_VALUE.md`**, **`WEAPON_DAMAGE.md`** — static-data location maps (track across updates).
|
||||
- **`SCRAPE_RUNBOOK.md`** — read-only live-scrape steps for when a playtest is online.
|
||||
- **`GHIDRA.md`** — headless Ghidra on `GameAssembly.dll`: **inject Il2CppDumper symbols, don't full-analyze** (`ghidra/scripts/apply_il2cpp_symbols.py`); targeted decompile/disasm; the `_JAVA_OPTIONS` heap gotcha. **The named DB is already built at `ghidra/project/SAND`** (564k methods, git-ignored/local) — decompile any function on demand via `-process … -postScript decomp_targets.py`.
|
||||
- **`BUNDLES.md`** (repo root) — inventory of the 35 asset bundles.
|
||||
|
||||
Operator memory lives in `~/.claude/projects/-home-downloadpizza-sand-tools/memory/` (loaded each session).
|
||||
@@ -1,5 +1,14 @@
|
||||
# Backend = PlayFab (Azure) — acquiring server-authoritative data
|
||||
|
||||
> **CORRECTIONS (2026-06-15, from a live playtest + code-verified RE — these override claims below):**
|
||||
> - **PlayFab is auth-only for this title.** Economy v2 is disabled (`403 "Catalog is not enabled
|
||||
> for this title"`); the classic catalog/inventory are empty. The economy (compartment stats,
|
||||
> prices, research) is **not** in PlayFab — it's on the master server. PlayFab does
|
||||
> `LoginWithSteam` + profile + one `TitleData` walker blueprint (`TramplerBlueprint3`). TitleId = **56693**.
|
||||
> - **Master server is NOT gRPC/SignalR and NOT cleartext `ws://:80`.** It's a custom per-call
|
||||
> WebSocket RPC over **`wss://:443`**, JSON, auth = PlayFab `SessionTicket` as a query param of the
|
||||
> `Login` op. Full spec + scraper: **[MASTER_SERVER.md](MASTER_SERVER.md)** (`reverse/master_scrape.py`).
|
||||
|
||||
## Observed boot behavior — no active playtest (GROUND TRUTH, reported by user)
|
||||
|
||||
When no playtest is running, the exact, observed sequence is:
|
||||
|
||||
96
docs/GHIDRA.md
Normal file
96
docs/GHIDRA.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Ghidra headless on SAND's `GameAssembly.dll` (IL2CPP)
|
||||
|
||||
How to get a workable Ghidra database for the client, and the **big lesson**: for an IL2CPP binary
|
||||
you **inject the symbol table from Il2CppDumper** — you do *not* sit through full auto-analysis.
|
||||
|
||||
> **CURRENT STATE (2026-06-16): the DB is already built and ready** at `ghidra/project/SAND`
|
||||
> (~945 MB, **564,713 methods named**, 294,174 function boundaries, 32,958 string labels; symbol-inject,
|
||||
> no auto-analysis). `ghidra/` is git-ignored so it's local/machine-specific — if it's missing, rebuild
|
||||
> with the import command below (~17 min). Decompile any target right now via the `-process` command in
|
||||
> "After the DB exists". (First real use: confirmed the master-server WS has **no cert pinning** — see
|
||||
> `docs/MASTER_SERVER.md` / TLS notes.)
|
||||
|
||||
## Inputs (all already on disk)
|
||||
- Binary: `/mnt/d/SteamLibrary/steamapps/common/Sand Playtest/GameAssembly.dll` (~137 MB).
|
||||
- **Il2CppDumper ("yoten")**: `/mnt/c/Users/downloadpizza/Downloads/yoten/` — produces, for the
|
||||
current build:
|
||||
- `script.json` (~254 MB) — the **mapping**: `ScriptMethod[]` (Address+Name+Signature),
|
||||
`ScriptString[]`, `ScriptMetadata[]`, `Addresses[]` (all function starts).
|
||||
- `il2cpp.h` (~124 MB) — every struct/type.
|
||||
- `dump.cs` (~79 MB) — human-readable signatures/RVAs (mirrored to `il2cpp/dump.cs`).
|
||||
- Ready-made apply scripts: `ghidra.py` (names only), `ghidra_with_struct.py` (names+types+sigs),
|
||||
plus IDA variants. **These use an interactive `askFile()` dialog → not headless-safe as shipped.**
|
||||
- Ghidra 11.1.2 install: `ghidra/ghidra_install/` (`support/analyzeHeadless`). Java 17.
|
||||
|
||||
## The lesson: symbol-inject, not full analysis
|
||||
Full auto-analysis of a 137 MB IL2CPP binary is the **wrong default**:
|
||||
- The **Decompiler Parameter ID** analyzer is essentially single-threaded and runs over hundreds of
|
||||
thousands of functions. Observed: **5h21m wall / ~5h40m CPU, pegged at ~105% (one core), with the
|
||||
log silent for 4.5h and no checkpoint** — headless saves the project **only at the very end**, so a
|
||||
crash/OOM mid-run loses everything. No progress %/ETA is emitted.
|
||||
- It largely **rediscovers** what Il2CppDumper already knows exactly (function boundaries, names,
|
||||
signatures). For our targeted-decompile workflow that's wasted time.
|
||||
|
||||
Instead: import with `-noanalysis` and run the dumper's symbol table in. You get a named,
|
||||
function-bounded DB in well under an hour. On-demand decompilation (`decomp_targets.py`) does its own
|
||||
per-function local analysis, so the global analyzers aren't needed for reading code.
|
||||
|
||||
### Headless-adapted applier — `ghidra/scripts/apply_il2cpp_symbols.py`
|
||||
Adapted from `yoten/ghidra.py`: replaced `askFile()` with a script-arg / default path. Light path —
|
||||
creates functions from `Addresses[]`, names them from `ScriptMethod[]`, labels string literals and
|
||||
metadata. **No `il2cpp.h` import, no signatures** (those need the type archive; see below).
|
||||
|
||||
```bash
|
||||
cd /home/downloadpizza/sand_tools
|
||||
# fresh project, import without analysis, inject symbols, save (background + log):
|
||||
rm -rf ghidra/project; mkdir -p ghidra/project
|
||||
_JAVA_OPTIONS= nohup ghidra/ghidra_install/support/analyzeHeadless ghidra/project SAND \
|
||||
-import "/mnt/d/SteamLibrary/steamapps/common/Sand Playtest/GameAssembly.dll" \
|
||||
-noanalysis -overwrite \
|
||||
-scriptPath ghidra/scripts -postScript apply_il2cpp_symbols.py \
|
||||
> ghidra/headless_symbols.log 2>&1 &
|
||||
# optional: pass a different script.json path as the postScript arg.
|
||||
```
|
||||
|
||||
### After the DB exists: targeted decompile / disasm (instant, no re-analysis)
|
||||
Put `rva<TAB>name` lines in `ghidra/targets.txt`, then `-process` the saved program:
|
||||
```bash
|
||||
_JAVA_OPTIONS= ghidra/ghidra_install/support/analyzeHeadless ghidra/project SAND \
|
||||
-process GameAssembly.dll -noanalysis \
|
||||
-scriptPath ghidra/scripts -postScript decomp_targets.py \
|
||||
> ghidra/headless.log 2>&1
|
||||
# -> ghidra/decomp.c (or disasm_targets.py -> ghidra/disasm.txt)
|
||||
```
|
||||
`decomp_targets.py`/`disasm_targets.py` already `disassemble()`+`createFunction()` per target, so they
|
||||
work even on a bare `-noanalysis` import; with symbols injected they also resolve names/xrefs.
|
||||
|
||||
## Typed decompiles (optional, heavy)
|
||||
For params shown as real types (`WalkerBlueprintDto *` …) use the `ghidra_with_struct.py` path: it
|
||||
imports `il2cpp.h` (124 MB) into Ghidra's `DataTypeManager` via the C parser **first**, then applies
|
||||
`ScriptMethod` signatures. The header parse is the slow / memory-hungry step (the usual OOM culprit).
|
||||
Usually unnecessary — `il2cpp/dump.cs` already has every signature for reference. Only do it if you
|
||||
specifically need typed struct fields in the decompiler.
|
||||
|
||||
## Address convention (verified)
|
||||
Il2CppDumper `script.json` `Address` = the Ghidra **offset from image base** directly:
|
||||
`baseAddress.add(Address)` (image base `0x180000000`). **No `-0x1000`.** (Note: the local
|
||||
`ghidra/methods.tsv` index used by `reverse/resolve_decomp.py` stores `rva = scriptAddress - 0x1000`
|
||||
for its own bookkeeping — different thing; don't conflate.)
|
||||
|
||||
## Memory / gotchas
|
||||
- `analyzeHeadless` has `MAXMEM=8G` (already bumped). **But the shell exports `_JAVA_OPTIONS=-Xmx4g`**,
|
||||
which silently caps the heap at 4 GB and causes swap thrash — always prefix runs with
|
||||
`_JAVA_OPTIONS=` to clear it. Machine has ~11 GiB RAM.
|
||||
- The run is detached via `nohup` (survives the session); it is **not** in tmux/screen. Watch with
|
||||
`tail -f ghidra/headless_symbols.log`. `REPORT: Save succeeded` = done.
|
||||
- `ghidra/` is git-ignored (install + project + dumps, all large/regenerable).
|
||||
|
||||
## Tooling map (`reverse/`, `ghidra/scripts/`)
|
||||
> `ghidra/` is git-ignored, so the **tracked masters** live in `reverse/` (`ghidra_*.py`); copy them
|
||||
> into `ghidra/scripts/` (where `-scriptPath` points) to run. e.g.
|
||||
> `cp reverse/ghidra_apply_il2cpp_symbols.py ghidra/scripts/apply_il2cpp_symbols.py`.
|
||||
- `reverse/ghidra_apply_il2cpp_symbols.py` → `ghidra/scripts/apply_il2cpp_symbols.py` — headless symbol injector (this doc).
|
||||
- `ghidra/scripts/decomp_targets.py` — decompile `targets.txt` → `ghidra/decomp.c`.
|
||||
- `ghidra/scripts/disasm_targets.py` — disassemble `targets.txt` → `ghidra/disasm.txt` (fast, no analysis).
|
||||
- `reverse/il2cpp_re.py` — VA↔file-offset, method index from `dump.cs`, xrefs, body disasm + float consts.
|
||||
- `reverse/resolve_decomp.py` — annotate `ghidra/decomp.c` with symbol names + string literals.
|
||||
75
docs/MASTER_SERVER.md
Normal file
75
docs/MASTER_SERVER.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Master server (`*.hologryph.com`) — protocol & scrape (WORKING)
|
||||
|
||||
Reverse-engineered from a fresh `il2cpp/dump.cs` + Ghidra decompile of the **2026-06-15 build
|
||||
(Steam buildid 23737037, GameAssembly.dll 143 MB)** and **verified live** — `reverse/master_scrape.py`
|
||||
pulls the full economy + stats from an external process (no game, no BattlEye interaction).
|
||||
|
||||
> Supersedes earlier guesses: PlayFab is auth-only (see [BACKEND_PLAYFAB.md](BACKEND_PLAYFAB.md));
|
||||
> the master server is **not** SignalR/gRPC and **not** cleartext `ws://:80`. The June-5 build used a
|
||||
> per-call WebSocket-per-op model; **today's build changed to the two-socket `ClientMessage` protocol
|
||||
> below** — re-RE was required after the playtest updated the client.
|
||||
|
||||
## Transport: `ClientMessage` over `wss://`, two sockets
|
||||
|
||||
Base region URL (live): `wss://ger.hologryph.com/gameclient/` (also eus/westus/sin/bra/aus/sko/uae;
|
||||
client picks one via region selection). Everything is JSON **text** frames of:
|
||||
|
||||
```
|
||||
ClientMessage = {"Id": <long>, "ClientMessageType": <int>, "Action": <int>, "Message": <string|null>}
|
||||
ClientMessageType: Error=0, Ping=1, Action=2, Event=3 (requests use 2; Ping must be echoed)
|
||||
Action (ClientAction enum): Login=0, Connect=1, GetCharacters=2, GetStorage=20, GetNews=21,
|
||||
GetCompartmentDefinitions=23, GetDatabaseGuid=26, GetDefaultWalkerBlueprints=28, GetServers=29,
|
||||
GetResearchTree=32, GetShopItems=39, ... (full list in il2cpp/dump.cs `enum ClientAction`)
|
||||
Message: the request payload as a JSON *string* (nested), or null for parameterless ops.
|
||||
```
|
||||
Serialization = Newtonsoft default: **PascalCase fields, enums as integers**. Each request gets an
|
||||
incrementing `Id`; the reply is a `ClientMessage` with the **same `Id`** (`_pendingRequests`
|
||||
correlates them), so replies may interleave with `Event(3)` pushes and `Ping(1)`.
|
||||
|
||||
Reply `Message` is either the **DTO directly** (e.g. Login → `LoginUserResultDto`) or an
|
||||
`OperationResult<T>` = `{IsSucceed,Error,Status,Result}` — `master_scrape.py` normalizes both.
|
||||
|
||||
## The handshake (this is the part that 1008s if wrong)
|
||||
|
||||
**Two sequential WebSocket connections:**
|
||||
|
||||
1. **`/login`** — connect `wss://<region>/gameclient/login` (NO auth header). Send
|
||||
`ClientMessage{Action=Login(0), Message=JSON(LoginRequest{SessionTicket, Title, Platform, ClientVersion})}`:
|
||||
- `SessionTicket` = the **PlayFab** SessionTicket (from `LoginWithSteam`; see playfab_scrape.py)
|
||||
- `Title` = PlayFab TitleId = `56693`; `Platform` = `1` (STEAM, int); `ClientVersion` = the build's
|
||||
**git commit hash** (e.g. `fbe5e4370daf86d54933ff4786b3312acd8c2d98`, from `Player.log`'s
|
||||
`Send Login … ver:'…'`; changes every build — server may version-gate → `Status=5 VERSION_MISMATCH`).
|
||||
- Reply `Message` = `LoginUserResultDto{User{Id,PlayFabId,PlatformId,DisplayName}, SessionTicket}`.
|
||||
**Keep that `SessionTicket`** (the server session ticket). Close the socket.
|
||||
2. **`/connect`** — connect `wss://<region>/gameclient/connect` **with HTTP upgrade header
|
||||
`Authorization: <server SessionTicket>`** (raw, no `Bearer `). Send `ClientMessage{Action=Connect(1),
|
||||
Message:"False"}` (the `isDebug` bool). This is the **persistent** socket; send all data ops over it.
|
||||
|
||||
Why the earlier attempts got `1008 "User is unauthorized!"`: connecting straight to `/connect` with no
|
||||
`Authorization` header (the auth is the header, set from the ticket that only `/login` mints). There is
|
||||
**no `SetRequestHeader` for `/login`** (its ticket rides inside the Login message instead).
|
||||
|
||||
## Data ops (over the `/connect` socket)
|
||||
|
||||
Send `ClientMessage{Action=<n>, Message:null}`; reply `Message` carries the result. Verified live:
|
||||
|
||||
| Action | n | Result |
|
||||
|---|---|---|
|
||||
| GetCompartmentDefinitions | 23 | `List<CompartmentDefinitionDto>` — EpbId, HP, Weight, VisualWeight, **Properties[{Key,Value}]**, CrownPrice, T1/T2/T3_MetalPrice (126 items) |
|
||||
| GetShopItems | 39 | `List<ShopItemDto>` — DefinitionName, Amount, BuyPrice[PriceDto] (54 items) |
|
||||
| GetResearchTree | 32 | `ResearchTreeJsonDto{Roots[3], Nodes[98]}` |
|
||||
| GetDefaultWalkerBlueprints | 28 | default walker blueprints |
|
||||
| GetServers / GetNews / GetDatabaseGuid / GetCharacters / GetStorage | 29/21/26/2/20 | server list / news / db guid / your characters / wallet+inventory |
|
||||
|
||||
`PriceDto = {ItemDefinition (currency item id), Amount}`; names resolved via `extracted/item_names.json`.
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
venv/bin/python reverse/master_scrape.py --selftest # offline
|
||||
venv/bin/python reverse/master_scrape.py # dry run (prints the exact frames)
|
||||
venv/bin/python reverse/master_scrape.py --go --data # login + global data -> extracted/master_*.json
|
||||
venv/bin/python reverse/master_scrape.py --go --user # + GetStorage / GetCharacters
|
||||
```
|
||||
Token from `reverse/.secrets/playfab_token.json` (gitignored): needs `SessionTicket` (fresh PlayFab
|
||||
login) + `ClientVersion` (current build's commit hash). Output → `extracted/master_<Action>.json`.
|
||||
67
docs/SCRAPE_RUNBOOK.md
Normal file
67
docs/SCRAPE_RUNBOOK.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Live-scrape runbook — when a playtest is online
|
||||
|
||||
Everything below is read-only and runs outside the game process (no BattlEye interaction).
|
||||
Tooling is built and unit-tested; the only thing that needs a live backend is the data itself.
|
||||
|
||||
## 0. Capture (once servers are up)
|
||||
|
||||
1. `ipconfig /flushdns` (so hostnames show as clean DNS queries, incl. the PlayFab TitleId).
|
||||
2. Start a packet capture on the game NIC (Wireshark, or `pktmon`/`dumpcap`). Save as `.pcapng`.
|
||||
- Master-server traffic is **cleartext `ws://` on port 80** — Wireshark reads it directly,
|
||||
**no MITM/cert needed**.
|
||||
- PlayFab is HTTPS/443 — to read its bodies you need your MITM (cert already installed) on 443,
|
||||
or use the REST scraper (step 3) instead.
|
||||
3. Launch SAND, **click through past the "no servers"/welcome dialog and let it log in**, then open
|
||||
the screens whose data you want (walker editor → compartment defs; research tree; store → prices).
|
||||
Keep capturing through it. Stop the capture.
|
||||
|
||||
## 1. Triage the capture → get the TitleId + confirm the master server
|
||||
|
||||
```bash
|
||||
venv/bin/python reverse/capture_hosts.py <capture.pcapng>
|
||||
```
|
||||
Prints DNS/SNI/endpoints in order and a **BACKENDS DETECTED** block:
|
||||
- `PlayFab host=<id>.playfabapi.com ** TitleId = <ID> **` ← the one constant the REST scraper needs
|
||||
- `Master server host=<region>.hologryph.com (ws://80 cleartext)`
|
||||
- `Config CDN host=sandconfigstorage…`
|
||||
|
||||
## 2. Master server (compartments + research tree + server list) — cleartext, no auth replay
|
||||
|
||||
```bash
|
||||
venv/bin/python reverse/ws_scrape.py <capture.pcapng> --out extracted/master_ws.json
|
||||
```
|
||||
Reassembles the port-80 WebSocket to `*.hologryph.com/gameclient/`, parses RFC-6455 frames, and
|
||||
decodes each message (tries JSON → BSON → MessagePack — the game's `IDataSerializer` is JSON-likely).
|
||||
Messages are auto-tagged when their shape matches a known DTO:
|
||||
`ServerDto`, `RegionInfo`, **`CompartmentDefinitionDto`** (HP/Weight/Properties/prices),
|
||||
**`ResearchNodeJsonDto`** (connections via `RequiredNodesIds`/`DependentNodesIds`, costs via
|
||||
`ResearchPrice`), `ItemDto`/`ShopItemDto`/`PriceDto`, `OperationResult`, `IClientEvent`.
|
||||
If it finds no WS stream, the capture didn't span the master-server connection (re-capture through
|
||||
the login), or try `--port`/`--host`.
|
||||
|
||||
> First run, eyeball one frame to confirm the encoding (JSON vs BSON). The decoder already handles
|
||||
> both; this is just a sanity check.
|
||||
|
||||
## 3. PlayFab prices / catalog / inventory
|
||||
|
||||
Either read them from the MITM'd 443 capture, **or** pull them directly (cleaner, gets the *full*
|
||||
catalog, more than the client requests):
|
||||
|
||||
```bash
|
||||
# with a Steam auth ticket (captured, or minted via Steamworks GetAuthSessionTicket):
|
||||
venv/bin/python reverse/playfab_scrape.py --title-id <ID> --steam-ticket <hex> --catalog --inventory
|
||||
|
||||
# or skip login with an EntityToken lifted from your MITM capture:
|
||||
venv/bin/python reverse/playfab_scrape.py --title-id <ID> --entity-token <tok> --catalog
|
||||
```
|
||||
`--catalog` → `extracted/playfab_catalog.json`: every item with `PriceOptions` (→ currency-item +
|
||||
amount, names resolved via `extracted/item_names.json`) and `DisplayProperties` (check here for any
|
||||
catalog-authored base stats). `--inventory` → wallet + items + transaction history. `--titledata`
|
||||
→ `Client/GetTitleData` config blobs. Read-only endpoints only — no write/purchase calls.
|
||||
|
||||
## Notes / unknowns to confirm live
|
||||
- **WS payload encoding** (JSON vs BSON): decoder handles both; confirm on first capture.
|
||||
- **Steam ticket reuse**: tickets are short-lived/single-use — if `--steam-ticket` fails, lift an
|
||||
`EntityToken` from the MITM capture and use `--entity-token` instead.
|
||||
- **Damage**: still server-computed; check `DisplayProperties` (catalog) and
|
||||
`CompartmentDefinitionDto.Properties` (master server) for any base values — don't assume present.
|
||||
149
docs/TRAMPLER.md
Normal file
149
docs/TRAMPLER.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Trampler / walker blueprints — structure, hashes, footprints, rendering
|
||||
|
||||
All findings from this session. Data-derived unless marked otherwise. Tooling in `reverse/`.
|
||||
|
||||
## Where blueprints come from
|
||||
- **Your own trampler**: master-server `GetTrampler` (Action 16, `Message`=characterId) or PlayFab
|
||||
`TitleData` (`TramplerBlueprint3`, a default). Shape = `WalkerBlueprintDto`.
|
||||
- **Another crew member's / the host's**: `GetExpedition` (Action 7) → `ExpeditionDto.Trampler`
|
||||
(a `WalkerDto`), or `GetExpeditionWalker(characterId)`. `WalkerDto` is the same build in a
|
||||
slightly different wrapper (see conversion below).
|
||||
- **Local saved walkers**: `.wbt` files in `…/Hologryph/Sand/Data/Walkers/*.wbt` (slightly different
|
||||
serialization — parsed separately; not covered here).
|
||||
|
||||
## `WalkerBlueprintDto` (the blueprint)
|
||||
```
|
||||
Id, UniqueId, Version,
|
||||
Chassis: CompartmentBlueprintDto,
|
||||
Compartments: [CompartmentBlueprintDto], # ORDER IS MEANINGFUL = the game's order (matches Id field)
|
||||
Connections: [ConnectionBlueprintDto],
|
||||
CompartmentsHash, DefinitionsHash, ConnectionsHash # 3 MD5 hashes
|
||||
```
|
||||
- `CompartmentBlueprintDto = {Id, EpbId, CellCoordinate{x,y,z}, DecorationsInfo, Rotation,
|
||||
CompartmentHash, DefinitionHash}` — `CellCoordinate` is the **origin/anchor cell**; `y` = floor.
|
||||
- `ConnectionBlueprintDto = {Id, EpbId(null), ActualDirection{x,y,z}, GridCoordinate{x,y,z},
|
||||
SlotType, State}`.
|
||||
- Compartment **order = the game's order** (verified: cargo C1–C8 matched in-game). Same as the `Id`
|
||||
field; arrays preserve it.
|
||||
|
||||
## `WalkerDto` → `WalkerBlueprintDto` conversion (`reverse/walkerdto_to_blueprint.py`)
|
||||
`WalkerDto = {Id, FirstNameIndex, SecondNameIndex, BlueprintId, BlueprintUniqueId, Chassis,
|
||||
Compartments:[CompartmentDto], Connections:[ConnectionDto]}` where `CompartmentDto={Id, Blueprint}`
|
||||
and `ConnectionDto={Id, Blueprint}`.
|
||||
- The WS form serializes **enums as ints and omits null EpbId**; the blueprint/hash ("storage") form
|
||||
uses **enum NAME strings** and includes `EpbId:null`. Convert: unwrap `.Blueprint`, map enums
|
||||
int→name, add `EpbId:null`. Enum maps (from dump.cs): `ConnectionSlotType`
|
||||
DOOR0/HATCH1/STRUCTURE2/BALCONY3/DECK4; `ConnectionState` DEFAULT0/DOOR1/OPEN2;
|
||||
`ConnectionsCount` FULL0/PARTIAL1/ERROR2.
|
||||
- Verified by a **storage→WS→storage round-trip** on the sample (byte-identical, hashes reproduce).
|
||||
|
||||
## The 5 hashes (`reverse/trampler_hashes.py`, `walker/walker_hashes.py`)
|
||||
`MD5Utility.ComputeHash(obj) = MD5( UTF8( JsonConvert.SerializeObject(obj) ) )` → **uppercase hex,
|
||||
no separators** (`X2`). Newtonsoft compact: no whitespace, PascalCase, declaration-order keys, nulls
|
||||
included, enums as **name strings**. A `WalkerBlueprintDto` carries **3 top-level** hashes; each
|
||||
`CompartmentBlueprintDto` (chassis + every part) carries **2 more**.
|
||||
|
||||
| Hash | Scope | What it hashes | Offline-computable? |
|
||||
|------|-------|----------------|---------------------|
|
||||
| `CompartmentsHash` | top-level | `MD5(compact-JSON(Compartments list))` | **YES** — VERIFIED |
|
||||
| `ConnectionsHash` | top-level | `MD5(compact-JSON(Connections list))` | **YES** — VERIFIED |
|
||||
| `DefinitionsHash` | top-level | `MD5(JSON(Compartments→CompartmentDefinitionDto))` | **NO** — server-sourced |
|
||||
| `CompartmentHash` | per-part | placement fingerprint (constant per EpbId+placement) | **NO from scratch** — harvest |
|
||||
| `DefinitionHash` | per-part | `MD5(JSON(that part's CompartmentDefinitionDto))` | **NO** — server-sourced |
|
||||
|
||||
- The two **Definition** hashes (`DefinitionsHash` + per-part `DefinitionHash`) hash the rich
|
||||
server-side `CompartmentDefinitionDto` — which is **NOT** in the blueprint and **NOT** equal to the
|
||||
server's `GetCompartmentDefinitions` *pricing* DTO (brute-forcing every field subset of that DTO
|
||||
reproduced neither). They can only be **harvested** (every part placed in the in-game editor writes
|
||||
them into the `.wbt` save) → `extracted/definition_hashes_known.json`, `walker/harvest_hashes.py`.
|
||||
- `CompartmentsHash` is a hash **over the Compartments list including each part's `CompartmentHash` +
|
||||
`DefinitionHash` fields** — so wiping per-part Definition hashes also changes `CompartmentsHash`.
|
||||
- Per-part `CompartmentHash` is **constant per EpbId** across placements (verified across 30 harvested
|
||||
EpbIds); per-part `DefinitionHash` is constant per EpbId **except** it varies with `DecorationsInfo`
|
||||
content (only `walker_compDeck_Square_Open_1x1_epb` seen to differ so far).
|
||||
|
||||
## Hash lifecycle & purpose — round-trip experiment (2026-06-16, live client)
|
||||
Test: built copies of a known walker offline with hashes wiped, loaded them in the game, watched
|
||||
`Player.log` (`CheckValidBlueprint: ERRORS {0}, VERSION:{1}, {2}` = `haveBuildingErrors`,
|
||||
`isVersionValid`, `UniqueId`), then opened/saved in the in-game editor and re-decoded.
|
||||
|
||||
| Variant | Comp/Conn hashes | Definition hashes | `ERRORS` | `VERSION` |
|
||||
|---------|------------------|-------------------|----------|-----------|
|
||||
| Definition hashes only wiped | valid | wiped | False | **True** |
|
||||
| **all 5 wiped** | wiped | wiped | False | **False** |
|
||||
| untouched original (current build) | valid | valid | False | True |
|
||||
| genuinely old-version walker | valid | valid | False | False |
|
||||
|
||||
Findings (data-derived):
|
||||
- **The client recomputes ALL FIVE hashes locally on save** from its own `CompartmentsDatabase` +
|
||||
`CompartmentDefinitionDto` container. Saving an all-wiped walker produced hashes **byte-identical** to
|
||||
the original source (same build ⇒ same hashes). So the hashes carry **no per-walker secret** and are a
|
||||
pure function of the build.
|
||||
- **Open/edit tolerates blank hashes**: an all-wiped walker loads, lists, and opens in the editor with
|
||||
no error; opening + closing **without saving rewrites nothing** (file stayed byte-identical). Hashes
|
||||
are (re)written **only on save**.
|
||||
- **Every save mints a new file UUID *and* a new `UniqueId`** (observed 3×).
|
||||
- **`isVersionValid` (the `VERSION` flag) depends only on the structural hashes** (`CompartmentsHash`/
|
||||
`ConnectionsHash`), **not** on the Definition hashes (def-only-wiped was still `VERSION:True`).
|
||||
`haveBuildingErrors` (`ERRORS`) is a separate structural check, independent of every hash.
|
||||
|
||||
Purpose (mechanism from `dump.cs`, not inferred): `WalkerBlueprintContainer.ValidateVersion(
|
||||
ICompartmentsDatabase, ICompartmentDefinitionDtoContainer)` sets `isVersionValid`/`haveBuildingErrors`.
|
||||
It takes the **current** database + definitions as input ⇒ the stored hashes are a **cached fingerprint
|
||||
re-checked against a fresh recompute**: when the game's part data changes across a patch, the recompute
|
||||
won't match the stored hash and the walker is flagged stale (`VERSION:False`, → migrate/re-save). They
|
||||
are **integrity / version-staleness markers, not security** — plain unsalted MD5 we reproduce offline;
|
||||
the client just regenerates them. From the *client editor's* view the **Definition** hashes appear inert
|
||||
(no effect on `ERRORS` or `VERSION`); whether the **server** validates them on upsert is still untested
|
||||
(the master-server `/connect` blocker is cleared as of 2026-06-16 — server back up — but the live
|
||||
re-test has not been run yet).
|
||||
|
||||
Practical upshot for offline editing: edit the `.wbt`, then either recompute the two structural hashes
|
||||
yourself (`walker/walker_hashes.py`, exact) or just let **one in-editor save** regenerate all five.
|
||||
|
||||
## Footprints (`extracted/CompartmentsDatabase.json` → `compartments[].cells[]`)
|
||||
Each compartment def has `cells[] = {position{x,y,z}, sockets{dir:{slotType:…}}, volumeOccupied,
|
||||
requireSupport}` in the compartment's **local frame**. World cells = rotate local `(x,z)` by the
|
||||
blueprint `Rotation`, add origin; `y` world = origin.y + local.y.
|
||||
- **Rotation convention (verified by 0-overlap check across 56 solid cells):**
|
||||
`90→(z,-x), 180→(-x,-z), 270→(-z,x)`.
|
||||
- `volumeOccupied=true` = solid cell; `false` = non-solid (e.g. the reactor's north half-circle dome).
|
||||
- Reactor `Round_Metal_2x1` is actually a 2-wide tower **rising y0→y5** with a 2×2 solid base + a
|
||||
1-row dome to the north — the EpbId "2x1" is not the real footprint.
|
||||
|
||||
## Connections (doors / hatches) — blueprint `Connections[]`
|
||||
Per slot: `GridCoordinate` (world cell), `ActualDirection` (face), `SlotType`, `State`.
|
||||
- **DOOR** (horizontal, `dir.y==0`): `State` DOOR/OPEN = real door, DEFAULT = wall.
|
||||
- **HATCH** (vertical, `dir.y==±1`): `State` DOOR = hatch between floors.
|
||||
- STRUCTURE / DECK / BALCONY = structural/walkable connections.
|
||||
|
||||
## Weapons
|
||||
- `BattleRam_2x2` = **rams** (no firing arc — battering rams). `TurretSlot_*` = turret mounts;
|
||||
`…OpenCorner…`/`…MetalCorner…` = **45° corner** mounts, others = straight/cardinal.
|
||||
- **Firing facing is NOT in the data** (weapon `Properties` empty; `CompartmentsDatabase` has no
|
||||
aim/forward/angle field). The renderer derives facing from the **`Rotation` field** + a
|
||||
user-supplied calibration (CALIBRATION, not pure data): straight rot0 = South, corner rot0 = SW,
|
||||
+90° = clockwise → `face = base − rotation` (base: straight 90°, corner 135°), arc = ±90°.
|
||||
|
||||
## Renderer (`reverse/render_trampler.py`)
|
||||
Per-floor (y −1…3) PNG: real footprints (solid vs faded non-solid), doors/hatches, single-tile guns
|
||||
with ±90° firing wedges, rams, cargo numbered **C1–C8** in JSON order. Outputs
|
||||
`extracted/host_trampler_*.png`. Reactor column above deck 3 and legs below the base are clipped.
|
||||
|
||||
## Other scrape tooling (this session)
|
||||
- `reverse/master_scrape.py` — master-server replay (two-socket `/login` + `/connect`; see
|
||||
[MASTER_SERVER.md](MASTER_SERVER.md)).
|
||||
- `reverse/{capture_hosts,ws_scrape,playfab_scrape,noise_filter,resolve_decomp}.py`.
|
||||
- Ghidra: `analyzeHeadless … -postScript decomp_targets.py|disasm_targets.py` (offset bug fixed:
|
||||
rva = il2cppdumper `Address` directly, **no −0x1000**).
|
||||
|
||||
## Open questions
|
||||
- **Server-side upsert validation — UNTESTED.** We've only confirmed the *client* tolerates wiped/blank
|
||||
hashes (loads, opens, edits, and regenerates all 5 on save). Whether the **master server** validates
|
||||
the hashes (esp. the server-sourced `DefinitionsHash` / per-part `DefinitionHash`) when a blueprint is
|
||||
`UpsertTrampler`'d — i.e. whether it recomputes, rejects, or stores blanks — is not yet known. It was
|
||||
previously blocked by the master server throwing `System.NullReferenceException` and closing the socket
|
||||
on `/connect` (no successful login); **that blocker is cleared as of 2026-06-16 — the master server is
|
||||
back up**, so the re-test is now unblocked but **has not been run** (awaiting explicit go-ahead for the
|
||||
live op). Re-test: upsert a walker with Definition hashes blank, then `GetTrampler` it back and diff.
|
||||
See [MASTER_SERVER.md](MASTER_SERVER.md).
|
||||
@@ -12,6 +12,25 @@ and the formula function (RVAs below); the literal constants need a different me
|
||||
end). This corrects an earlier draft that wrongly concluded "no value exists" — the values
|
||||
**are** live at runtime; they just aren't statically anchorable constants.
|
||||
|
||||
**Confirmed 2026-06-16 (offline, `bundle/dump_blueprint.py`): the `Damage*DataComponent`s are
|
||||
NOT authored on the item/ammo EntityBlueprints in the bundles.** Decoded `item_grenadeContact`,
|
||||
`item_shotgunAmmo`, `item_pistolAmmo`, `item_shotgun`, `item_revolverSmall_dusters` — every one
|
||||
has only generic components (InteractActions, Count, ItemName, ItemType, NiceName, View,
|
||||
ViewSize, colliders, Physics); **zero** `Damage{Physical,…}DataComponent` / `MeleeDataComponent`
|
||||
/ AoE. The only `.value` floats present are `ViewSizeDataComponent` (~0.3–0.97), not damage. So
|
||||
`GetDamage`'s `DamageXxxDataComponent.value` reads are populated **at runtime (server-authoritative)**
|
||||
— there is no per-weapon damage constant in the bundles *or* as a static anchor in the DLL.
|
||||
|
||||
**Ways to get actual base numbers, ranked:**
|
||||
- ✅ **In-game empirical measurement** (controlled damage tests) — the only clean route.
|
||||
- ⚠️ Live-client runtime memory (the components hold real values once spawned) — but that's
|
||||
process inspection → **BattlEye / no injection** → off-limits.
|
||||
- ❌ Static extraction (bundles) — values absent (proven above).
|
||||
- ❌ Static decompile constant — none exists (generic Entitas dispatch).
|
||||
- ❌ Master-server query — no damage field / no stats endpoint (see [MASTER_SERVER.md](MASTER_SERVER.md)).
|
||||
- ❓ Game-server (not master) entity-snapshot capture *might* carry component values, but unverified
|
||||
and the server may only transmit results, not per-weapon stats.
|
||||
|
||||
## Damage model (all static, all in `il2cpp/dump.cs` + verified by disasm)
|
||||
|
||||
Per-type damage lives as a `float value` (object offset **+0x10**) on 8 components on the
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,82 +0,0 @@
|
||||
# SAND extracted game data
|
||||
|
||||
Extracted from `D:\SteamLibrary\steamapps\common\Sand Playtest` (Addressables bundles) and
|
||||
the local walker saves. Tools: UnityPy + TypeTreeGeneratorAPI (in `~/sand_tools/venv`).
|
||||
|
||||
## Files
|
||||
|
||||
- **crafting_recipes.json** — all 4 crafting recipe bundles (Utility T1, Armament T1/T2, Test).
|
||||
Schema: `{bundleName: [{inputs:[{id,count}], outputs:[{id,count}], craftTime}]}`.
|
||||
Source: `craftingrecipes_assets_all.bundle` (4 MonoBehaviours).
|
||||
|
||||
- **CompartmentsDatabase.json** — the full compartment placement DB (126 parts: cells, sockets,
|
||||
groups, pivots). Source: `CompartmentsDatabase` TextAsset in `walkershared_assets_all.bundle`.
|
||||
|
||||
- **compartment_hashes.json** — `{entityId: {CompartmentHash, placement}}` for all 126 parts.
|
||||
`CompartmentHash = MD5(UTF8(JsonConvert.SerializeObject(placement, compact)))` — VERIFIED
|
||||
against every part in the local walkers. This is the authoritative current CompartmentHash.
|
||||
|
||||
- **definition_hashes_known.json** — per-part hashes OBSERVED in the 5 local walker saves
|
||||
(18 parts). Has both observed CompartmentHash/DefinitionHash and the computed CompartmentHash.
|
||||
|
||||
- **item_definitions.json** — the MASTER ITEM LIST: 121 item ids → category (WEAPON, AMMO,
|
||||
RESOURCE_T1/2/3, KEY, ARMOR, ENERGY, FOOD, MONEY, …18 categories). Parsed from the
|
||||
`CheatItemDefinitionsData` MonoBehaviour in `configuration_assets_all.bundle`.
|
||||
|
||||
- **epb_catalog.json** — all 1446 entity blueprints (EPBs) by category: walker compartments
|
||||
(156), chassis (22), weapons, turret containers, crates, props, spawners, AI, items, etc.
|
||||
Source: `epb_assets_all.bundle` container paths. (Walker buildable parts = 126 of the 156
|
||||
comp EPBs; see CompartmentsDatabase.json for the placeable set.)
|
||||
|
||||
- **i2_terms_en.json** — the FULL I2 Localization term table (English), 3440 terms keyed like
|
||||
`Items/item_X_name`, `WalkerCompartments/..._name/_description`, `WalkerFirstName/...`, etc.
|
||||
Source: the `I2Languages` LanguageSourceAsset baked into `Sand_Data/data.unity3d`.
|
||||
|
||||
- **item_names.json** — real in-game item names + descriptions: `{itemId: {name, shortDescription,
|
||||
description}}` for 642 named entries from the `Items/*` I2 terms. NOTE: this is the WIDE set — it
|
||||
includes damage-type name variants (`_Ranged`/`_Melee`) and world objects that merely have a
|
||||
display name. For the true item list use items_registry.json.
|
||||
|
||||
- **items_registry.json** — the AUTHORITATIVE carriable-item list: 121 items, each with `type`
|
||||
(ItemType), `storageStack`, real `name`, `shortDescription`, `description`. Source: the game's
|
||||
only `List<ItemDefinition>` (`CheatItemDefinitionsData.Items`, read via IL2CPP typetree) merged
|
||||
with I2 names. An item is here iff the game defines it as a real inventory item — this is the
|
||||
principled "what can be picked up" filter (excludes `_Ranged`/`_Melee` variants + world objects).
|
||||
(2 craftable outputs — `ArtefactCrystal`, `item_rifleMusket` — lack an ItemDefinition; likely
|
||||
WIP/renamed.) The Items wiki page is built from this.
|
||||
|
||||
- **loot_tables.json** — the DROP TABLES, decoded from the two Odin-binary `LootTablesConfig`
|
||||
assets (`conf_worldLootTablesStormConfig` / `conf_worldLootTablesVoyageConfig`). Schema:
|
||||
`{region: {lootTableId: [{itemBlueprint, countMin, countMax}]}}`. 193 tables per region (Storm,
|
||||
Voyage), 696 drop rows each. Both regions share the same table ids but 152/193 differ in counts
|
||||
(Voyage is leaner). Decoded by `~/sand_tools/odin_read.py` (a from-scratch Sirenix Odin Binary
|
||||
reader) + `~/sand_tools/extract_loot.py`. Raw payloads: `conf_worldLootTables*Config.odin.bin`.
|
||||
|
||||
- **Crafting.mediawiki** — ready-to-paste MediaWiki page for all crafting recipes, using the real
|
||||
item names. Generated by `~/sand_tools/make_crafting_wiki.py`.
|
||||
|
||||
## Crafting / workbench notes
|
||||
|
||||
- 4 recipe bundles: `Recipes_Utility_Workbench_T1` (fabric/armour/food/valuables), `Recipes_Armament_Workbench_T1`
|
||||
and `_T2` (ammo/weapons), `TestRecipesBundle` (debug).
|
||||
- Placeable crafting compartments (in CompartmentsDatabase, all enabled): `walker_compCrafting_Open_1x1`
|
||||
= "S&H Armaments Workbench", `_Small_Wood_1x1` = "S&H Compact Armaments Workshop", `_Wood_2x1`
|
||||
= "S&H Armaments Workshop". The "KF Sewing Workshop" (`walker_compCraftingUtility_Wood_2x1`,
|
||||
utility/fabric) is NOT in the buildable list in this build.
|
||||
- The compartment→recipe-bundle binding is set at runtime in (obfuscated) game code, NOT in the
|
||||
asset data — the EPB CraftingWorkbench components carry no PPtr to the bundles. So which placed
|
||||
workbench loads which bundle is inferred, not statically proven.
|
||||
|
||||
## Hash facts
|
||||
|
||||
All walker hashes = `MD5(UTF8(JsonConvert.SerializeObject(obj)))`, uppercase hex.
|
||||
- `CompartmentHash` (per part): obj = CompartmentPlacementInfo (from CompartmentsDatabase). DONE.
|
||||
- `DefinitionHash` (per part): obj = CompartmentDefinitionDto {EpbId, Weight, VisualWeight, HP,
|
||||
Properties[{Key,Value}], CrownPrice, T1/T2/T3_MetalPrice}. **SERVER-SOURCED — not reproducible
|
||||
offline.** `ClientCompartmentDefinitionsContainer.GetDefinitionsAsync` fetches the DTO list from
|
||||
the masterserver (`ClientMasterServerNetwork.GetCompartmentDefinitions`) and hashes the response;
|
||||
the stats are not in local assets. Only the 18 values observed in local walkers are known (see
|
||||
definition_hashes_known.json). To get more: build the part in-game once (its hash lands in the
|
||||
save) or capture the masterserver definitions response.
|
||||
- Aggregate `CompartmentsHash`/`DefinitionsHash`/`ConnectionsHash` (per walker): MD5 of a
|
||||
Newtonsoft-serialized object; exact input not yet pinned down.
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -1,491 +0,0 @@
|
||||
{
|
||||
"_source": "islands_assets_all.bundle island_* prefab hierarchy (game_conveyor_<product>_epb child GameObjects); recipes from production_lines.json",
|
||||
"_unplaced_recipes": [
|
||||
"game_conveyor_armorPiercingRockets",
|
||||
"game_conveyor_explosiveSmall",
|
||||
"game_conveyor_mechanicalParts"
|
||||
],
|
||||
"islands": {
|
||||
"island_Demo_Wunderinsel": {
|
||||
"recipes": [
|
||||
{
|
||||
"conveyor": "game_conveyor_70mmT3Cannon",
|
||||
"pos": [
|
||||
-3.27,
|
||||
-0.01,
|
||||
-7.78
|
||||
],
|
||||
"recipe": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_alloySteel",
|
||||
"amount": 40
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetal_t2",
|
||||
"amount": 300
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "game_packedShotgunTurretT3Container",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 55.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"product_conveyors_without_matching_recipe": [],
|
||||
"infrastructure": [
|
||||
"game_conveyorInSwitch_epb",
|
||||
"game_conveyorOutLargeItem_epb",
|
||||
"game_conveyorSlot"
|
||||
]
|
||||
},
|
||||
"island_DeusExMashineSmall": {
|
||||
"recipes": [
|
||||
{
|
||||
"conveyor": "game_conveyor_computingModules",
|
||||
"pos": [
|
||||
19.04,
|
||||
-0.34,
|
||||
-11.85
|
||||
],
|
||||
"recipe": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_blackBox",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_resourceMetal_t3",
|
||||
"amount": 10
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 7.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"conveyor": "game_conveyor_coralDust",
|
||||
"pos": [
|
||||
0.9,
|
||||
0.0,
|
||||
3.2
|
||||
],
|
||||
"recipe": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceCoralPiece",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_resourceCoralDust",
|
||||
"amount": 10
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetal_t1",
|
||||
"amount": 2
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"product_conveyors_without_matching_recipe": [],
|
||||
"infrastructure": [
|
||||
"game_conveyorInSwitch_epb",
|
||||
"game_conveyorOutInventory_epb",
|
||||
"game_conveyorSlotInput_epb"
|
||||
]
|
||||
},
|
||||
"island_Factorio": {
|
||||
"recipes": [
|
||||
{
|
||||
"conveyor": "game_conveyor_energyRods",
|
||||
"pos": [
|
||||
-10.25,
|
||||
-3.44,
|
||||
-0.51
|
||||
],
|
||||
"recipe": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_crystalHandles",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_energyBar",
|
||||
"amount": 10
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 7.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"conveyor": "game_conveyor_80mmT3Cannon",
|
||||
"pos": [
|
||||
3.49,
|
||||
-3.45,
|
||||
-7.43
|
||||
],
|
||||
"recipe": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_alloySteel",
|
||||
"amount": 40
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetal_t2",
|
||||
"amount": 300
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "game_packedTurretT3Container",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 55.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"conveyor": "game_conveyor_contactGrenades",
|
||||
"pos": [
|
||||
1.25,
|
||||
3.52,
|
||||
1.93
|
||||
],
|
||||
"recipe": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceFabric",
|
||||
"amount": 10
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceGunpowder",
|
||||
"amount": 10
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_grenadeContact",
|
||||
"amount": 5
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"product_conveyors_without_matching_recipe": [
|
||||
{
|
||||
"conveyor": "game_conveyor_armorPiercingRocket",
|
||||
"pos": [
|
||||
-8.05,
|
||||
3.52,
|
||||
-8.79
|
||||
]
|
||||
}
|
||||
],
|
||||
"infrastructure": [
|
||||
"game_conveyorInSwitch_epb",
|
||||
"game_conveyorOutInventory_epb",
|
||||
"game_conveyorOutLargeItem_epb",
|
||||
"game_conveyorSlot",
|
||||
"game_conveyorSlotInput_epb"
|
||||
]
|
||||
},
|
||||
"island_Kaiserplatz": {
|
||||
"recipes": [
|
||||
{
|
||||
"conveyor": "game_conveyor_energyRods",
|
||||
"pos": [
|
||||
3.06,
|
||||
0.05,
|
||||
1.41
|
||||
],
|
||||
"recipe": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_crystalHandles",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_energyBar",
|
||||
"amount": 10
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 7.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"conveyor": "game_conveyor_40mmT3Cannon",
|
||||
"pos": [
|
||||
1.13,
|
||||
0.05,
|
||||
-6.21
|
||||
],
|
||||
"recipe": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_alloySteel",
|
||||
"amount": 40
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetal_t2",
|
||||
"amount": 300
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "game_packedAutoTurretT3Container",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 55.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"product_conveyors_without_matching_recipe": [],
|
||||
"infrastructure": [
|
||||
"game_conveyorInSwitch_epb",
|
||||
"game_conveyorOutInventory_epb",
|
||||
"game_conveyorOutLargeItem_epb",
|
||||
"game_conveyorSlot",
|
||||
"game_conveyorSlotInput_epb"
|
||||
]
|
||||
},
|
||||
"island_testIsland": {
|
||||
"recipes": [
|
||||
{
|
||||
"conveyor": "game_conveyor_baseInventoryToLarge",
|
||||
"pos": [
|
||||
0.0,
|
||||
0.1,
|
||||
0.0
|
||||
],
|
||||
"recipe": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceMetal_t1",
|
||||
"amount": 1
|
||||
},
|
||||
{
|
||||
"itemId": "item_c4Dynamite",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_explosiveBig",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 15.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"conveyor": "game_conveyor_baseInventoryToInventory",
|
||||
"pos": [
|
||||
-0.0,
|
||||
0.0,
|
||||
-6.0
|
||||
],
|
||||
"recipe": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceMetal_t1",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_c4Dynamite",
|
||||
"amount": 1
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetal_t1",
|
||||
"amount": 2
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"conveyor": "game_conveyor_baseLargeToLarge",
|
||||
"pos": [
|
||||
7.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"recipe": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_wineBox",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_explosiveBig",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 15.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"conveyor": "game_conveyor_baseLargeToInventory",
|
||||
"pos": [
|
||||
5.0,
|
||||
7.5,
|
||||
-0.0
|
||||
],
|
||||
"recipe": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_wineBox",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_coinCrown",
|
||||
"amount": 1000
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 15.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"product_conveyors_without_matching_recipe": [],
|
||||
"infrastructure": [
|
||||
"game_conveyorInSwitch_epb",
|
||||
"game_conveyorOutInventory_epb",
|
||||
"game_conveyorOutLargeItem_epb",
|
||||
"game_conveyorSlot",
|
||||
"game_conveyorSlotInput_epb"
|
||||
]
|
||||
},
|
||||
"island_testIslandTramplers": {
|
||||
"recipes": [
|
||||
{
|
||||
"conveyor": "game_conveyor_baseInventoryToLarge",
|
||||
"pos": [
|
||||
0.0,
|
||||
0.1,
|
||||
0.0
|
||||
],
|
||||
"recipe": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceMetal_t1",
|
||||
"amount": 1
|
||||
},
|
||||
{
|
||||
"itemId": "item_c4Dynamite",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_explosiveBig",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 15.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"conveyor": "game_conveyor_baseInventoryToInventory",
|
||||
"pos": [
|
||||
-0.0,
|
||||
0.0,
|
||||
-6.0
|
||||
],
|
||||
"recipe": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceMetal_t1",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_c4Dynamite",
|
||||
"amount": 1
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetal_t1",
|
||||
"amount": 2
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"conveyor": "game_conveyor_baseLargeToLarge",
|
||||
"pos": [
|
||||
7.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"recipe": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_wineBox",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_explosiveBig",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 15.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"conveyor": "game_conveyor_baseLargeToInventory",
|
||||
"pos": [
|
||||
5.0,
|
||||
7.5,
|
||||
-0.0
|
||||
],
|
||||
"recipe": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_wineBox",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_coinCrown",
|
||||
"amount": 1000
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 15.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"product_conveyors_without_matching_recipe": [],
|
||||
"infrastructure": [
|
||||
"game_conveyorInSwitch_epb",
|
||||
"game_conveyorOutInventory_epb",
|
||||
"game_conveyorOutLargeItem_epb",
|
||||
"game_conveyorSlot",
|
||||
"game_conveyorSlotInput_epb"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,615 +0,0 @@
|
||||
{
|
||||
"_source": "CraftingRecipeBundle MonoBehaviours via IL2CPP typetree (Unity 6000.0.40f1)",
|
||||
"recipes": {
|
||||
"Recipes_Utility_Workbench_T1": [
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceFabricScraps",
|
||||
"amount": 5
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceThreads",
|
||||
"amount": 15
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_resourceFabric",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 2.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceFabric",
|
||||
"amount": 2
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceThreads",
|
||||
"amount": 10
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "Old_Jacket",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceFabric",
|
||||
"amount": 5
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceThreads",
|
||||
"amount": 50
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "Old_JacketT2",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_crystalHandles",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "ArtefactCrystal",
|
||||
"amount": 4
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_documentSafe",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_valuablePapers",
|
||||
"amount": 150
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_cannedFish",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_foodCan",
|
||||
"amount": 25
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
}
|
||||
],
|
||||
"Recipes_Armament_Workbench_T1": [
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceScrappedAmmo",
|
||||
"amount": 5
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetalParts",
|
||||
"amount": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_pistolAmmo",
|
||||
"amount": 20
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 3.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceScrappedAmmo",
|
||||
"amount": 5
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceFabricScraps",
|
||||
"amount": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_shotgunAmmo",
|
||||
"amount": 10
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 3.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceMetalParts",
|
||||
"amount": 5
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceScrappedAmmo",
|
||||
"amount": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_rifleAmmo",
|
||||
"amount": 10
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 3.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceFabricScraps",
|
||||
"amount": 5
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceScrappedAmmo",
|
||||
"amount": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_shotgunTurretAmmo",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 2.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceScrappedAmmo",
|
||||
"amount": 5
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetalParts",
|
||||
"amount": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_turretAmmo",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 2.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceScrappedAmmo",
|
||||
"amount": 2
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetalParts",
|
||||
"amount": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_smallCannonAmmo",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 1.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceMetalParts",
|
||||
"amount": 25
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_revolverSmall",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceMetalParts",
|
||||
"amount": 25
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_shotgunHandmade",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceMetalParts",
|
||||
"amount": 25
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_rifleMusket",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceMetalParts",
|
||||
"amount": 25
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_semiAutomaticPistol_decreasedMag",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_shotgunHandmade",
|
||||
"amount": 1
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetalParts",
|
||||
"amount": 25
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_shotgunHandmade_choke",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_revolverSmall",
|
||||
"amount": 1
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetalParts",
|
||||
"amount": 25
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_revolverSmall_dusters",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
}
|
||||
],
|
||||
"TestRecipesBundle": [
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_pistolAmmo",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_coinCrown",
|
||||
"amount": 5
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 1.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_pistolAmmo",
|
||||
"amount": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_coinCrown",
|
||||
"amount": 25
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 3.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_pistolAmmo",
|
||||
"amount": 100
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_coinCrown",
|
||||
"amount": 1000
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 10.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_coinCrown",
|
||||
"amount": 10
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_c4Dynamite",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 30.0
|
||||
}
|
||||
],
|
||||
"Recipes_Armament_Workbench_T2": [
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceHighGradeGunpowder",
|
||||
"amount": 1
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetalParts",
|
||||
"amount": 25
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_sniperRifleAmmo",
|
||||
"amount": 6
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 2.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceFabric",
|
||||
"amount": 4
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetalRods",
|
||||
"amount": 4
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_shotgun",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceMetalRods",
|
||||
"amount": 4
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceWeaponParts",
|
||||
"amount": 4
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_repeaterRifle",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceMetalRods",
|
||||
"amount": 4
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceWeaponParts",
|
||||
"amount": 4
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_semiAutomaticPistol",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_sniperRifle_ironSights",
|
||||
"amount": 1
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceOpticLenses",
|
||||
"amount": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_sniperRifle",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 10.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_sniperRifle_ironSights",
|
||||
"amount": 1
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetalRods",
|
||||
"amount": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_sniperRifle_ironSights_silencer",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_sniperRifle",
|
||||
"amount": 1
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetalRods",
|
||||
"amount": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_sniperRifle_silencer",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_sniperRifle_ironSights_silencer",
|
||||
"amount": 1
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceOpticLenses",
|
||||
"amount": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_sniperRifle_silencer",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceFabric",
|
||||
"amount": 2
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceGunpowder",
|
||||
"amount": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_c4Dynamite",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 3.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_pistolAmmo",
|
||||
"amount": 20
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceCoralDust",
|
||||
"amount": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_pistolAmmo_highVelocity",
|
||||
"amount": 20
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 3.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_rifleAmmo",
|
||||
"amount": 10
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceCoralDust",
|
||||
"amount": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_rifleAmmo_highVelocity",
|
||||
"amount": 10
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 3.0
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_shotgunAmmo",
|
||||
"amount": 10
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetalRods",
|
||||
"amount": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_shotgunAmmo_slug",
|
||||
"amount": 10
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 3.0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
{
|
||||
"_note": "Per-part hashes harvested from walker saves. CompartmentHash is also computed offline from CompartmentsDatabase (CompartmentHash_computed, authoritative). DefinitionHash is server-sourced and only known for parts that appear in a save \u2014 build a part in-game and re-run this to capture it.",
|
||||
"definition_known": 19,
|
||||
"total_parts_in_db": 126,
|
||||
"parts": {
|
||||
"walker_compCaptainCrew_Wood_1x1_epb": {
|
||||
"DefinitionHash": "37F659D0A1EDBFEF11138B059BA87630",
|
||||
"DefinitionHash_history": [
|
||||
"37F659D0A1EDBFEF11138B059BA87630",
|
||||
"B309519F174BCDFB7138893F1F7F30D3"
|
||||
],
|
||||
"CompartmentHash": "3FD5BF5BBCDAF575BD597F4E07C103AC",
|
||||
"CompartmentHash_history": [
|
||||
"CA0A7339EC20ADAAD07B756BA3206F77",
|
||||
"2EBA048C729D7B8BC1E0B5AD505AC1A0"
|
||||
],
|
||||
"CompartmentHash_computed": "CA0A7339EC20ADAAD07B756BA3206F77"
|
||||
},
|
||||
"walker_compCargo_SmallL_Open_1x1_epb": {
|
||||
"DefinitionHash": "85F5F699C2E54A7AEDA71BEC47247C8A",
|
||||
"DefinitionHash_history": [],
|
||||
"CompartmentHash": "792FC2A3772E72A708ECD052F967EF43",
|
||||
"CompartmentHash_history": [],
|
||||
"CompartmentHash_computed": "792FC2A3772E72A708ECD052F967EF43"
|
||||
},
|
||||
"walker_compCargo_SmallP_Open_1x1_epb": {
|
||||
"DefinitionHash": "FDE0DC860556B47C20519AA591C78C53",
|
||||
"DefinitionHash_history": [],
|
||||
"CompartmentHash": "7F34C8F33A98AFFBFFADE7C70D7E839D",
|
||||
"CompartmentHash_history": [],
|
||||
"CompartmentHash_computed": "7F34C8F33A98AFFBFFADE7C70D7E839D"
|
||||
},
|
||||
"walker_compCargo_SmallSingle_Open_1x1_epb": {
|
||||
"DefinitionHash": "FAD8D9C10DABE3EAEE0EB419D0698854",
|
||||
"DefinitionHash_history": [],
|
||||
"CompartmentHash": "7AD5369B3F32A242F9619D94A62B9D43",
|
||||
"CompartmentHash_history": [],
|
||||
"CompartmentHash_computed": "7AD5369B3F32A242F9619D94A62B9D43"
|
||||
},
|
||||
"walker_compChassis_Small4_Metal_3x3_epb": {
|
||||
"DefinitionHash": "5C59A7BE12A7B6F9FF37694AE5D8CD0D",
|
||||
"DefinitionHash_history": [],
|
||||
"CompartmentHash": "FB02111D85638BDA30832D8182ADD707",
|
||||
"CompartmentHash_history": [],
|
||||
"CompartmentHash_computed": "FB02111D85638BDA30832D8182ADD707"
|
||||
},
|
||||
"walker_compChassis_SmallLong4_Metal_2x3_epb": {
|
||||
"DefinitionHash": "64677F77ED55788CA579CDC5D5EDC53C",
|
||||
"DefinitionHash_history": [
|
||||
"64677F77ED55788CA579CDC5D5EDC53C",
|
||||
"1AE0A864553F1F8BD0CC4C8464CFDA3D"
|
||||
],
|
||||
"CompartmentHash": "D1EBE5DDB86BD898DA22A558F5199922",
|
||||
"CompartmentHash_history": [],
|
||||
"CompartmentHash_computed": "D1EBE5DDB86BD898DA22A558F5199922"
|
||||
},
|
||||
"walker_compCrafting_Open_1x1_epb": {
|
||||
"DefinitionHash": "BF09B8F2464B7F2C2375663CCD15A847",
|
||||
"DefinitionHash_history": [
|
||||
"E7C4E80FED44AA8E08D85A89EF592649"
|
||||
],
|
||||
"CompartmentHash": "6DBDB16FB8FEDA6158D6C96C04AD9CD6",
|
||||
"CompartmentHash_history": [],
|
||||
"CompartmentHash_computed": "6DBDB16FB8FEDA6158D6C96C04AD9CD6"
|
||||
},
|
||||
"walker_compCrafting_Small_Wood_1x1_epb": {
|
||||
"DefinitionHash": "50E4D77309174831337EF3874A036D02",
|
||||
"DefinitionHash_history": [],
|
||||
"CompartmentHash": "C341E8B3679D9E06C110F0E04045F1E5",
|
||||
"CompartmentHash_history": [],
|
||||
"CompartmentHash_computed": "C341E8B3679D9E06C110F0E04045F1E5"
|
||||
},
|
||||
"walker_compCrew_Medium_Wood_1x2_epb": {
|
||||
"DefinitionHash": "5A8DB326E5E77B756218B45711A8104F",
|
||||
"DefinitionHash_history": [],
|
||||
"CompartmentHash": "9B219D6F887F9CB6CBA05A4AFAE1A685",
|
||||
"CompartmentHash_history": [],
|
||||
"CompartmentHash_computed": "9B219D6F887F9CB6CBA05A4AFAE1A685"
|
||||
},
|
||||
"walker_compDeck_Square_Frame_1x1_epb": {
|
||||
"DefinitionHash": "97218AB6D0FDA2F5792DBCBBFD03798C",
|
||||
"DefinitionHash_history": [],
|
||||
"CompartmentHash": "6F5115567893669173249A4197E96522",
|
||||
"CompartmentHash_history": [],
|
||||
"CompartmentHash_computed": "6F5115567893669173249A4197E96522"
|
||||
},
|
||||
"walker_compDeck_Square_Open_1x1_epb": {
|
||||
"DefinitionHash": "04E304846A06AF8E3209DF1054D1E784",
|
||||
"DefinitionHash_history": [],
|
||||
"CompartmentHash": "0B8CC69BD1392E6EA7EA37BE5DE6C372",
|
||||
"CompartmentHash_history": [],
|
||||
"CompartmentHash_computed": "0B8CC69BD1392E6EA7EA37BE5DE6C372"
|
||||
},
|
||||
"walker_compDeck_Triangle_Frame_1x1_epb": {
|
||||
"DefinitionHash": "60EE31A862923F3D837377504D2C9517",
|
||||
"DefinitionHash_history": [
|
||||
"94F5A0C3D594857CB52A5C835479C120"
|
||||
],
|
||||
"CompartmentHash": "E369FD759A61DD7E47F2F67EBAFC311D",
|
||||
"CompartmentHash_history": [],
|
||||
"CompartmentHash_computed": "E369FD759A61DD7E47F2F67EBAFC311D"
|
||||
},
|
||||
"walker_compReactor_Round_Open_2x1_epb": {
|
||||
"DefinitionHash": "5744B05118A9CF470EEB77DC2FB8C62D",
|
||||
"DefinitionHash_history": [
|
||||
"5744B05118A9CF470EEB77DC2FB8C62D",
|
||||
"1C271BEB1E0AE8E0F92EE51EDC32E2B5"
|
||||
],
|
||||
"CompartmentHash": "BDADD1B66303873E30F8D031EE1F99A1",
|
||||
"CompartmentHash_history": [
|
||||
"BDADD1B66303873E30F8D031EE1F99A1",
|
||||
"163AC7D2EB9ADB76EBAFA1C613311535"
|
||||
],
|
||||
"CompartmentHash_computed": "BDADD1B66303873E30F8D031EE1F99A1"
|
||||
},
|
||||
"walker_compSpecial_Entry_Frame_1x1_epb": {
|
||||
"DefinitionHash": "CB2209E5C634FBC37C242DE4ED91B70C",
|
||||
"DefinitionHash_history": [],
|
||||
"CompartmentHash": "9FB555D20A1BADBBD3AD73DFC989F9B6",
|
||||
"CompartmentHash_history": [
|
||||
"9FB555D20A1BADBBD3AD73DFC989F9B6",
|
||||
"96C7E03FE93D00EF1486E9B5CADFD6F2"
|
||||
],
|
||||
"CompartmentHash_computed": "9FB555D20A1BADBBD3AD73DFC989F9B6"
|
||||
},
|
||||
"walker_compSteering_Square_Open_1x1_epb": {
|
||||
"DefinitionHash": "8AD614A669C43EC06EC49FB2C0C9F56D",
|
||||
"DefinitionHash_history": [
|
||||
"8AD614A669C43EC06EC49FB2C0C9F56D",
|
||||
"9CD3D12D646ED67EDEF610BB11F70577"
|
||||
],
|
||||
"CompartmentHash": "93901C77B060FA462EB1C415D7304E07",
|
||||
"CompartmentHash_history": [],
|
||||
"CompartmentHash_computed": "93901C77B060FA462EB1C415D7304E07"
|
||||
},
|
||||
"walker_compStructure_SupportTriangle_FrameLeft_1x1_epb": {
|
||||
"DefinitionHash": "7E915EB76815F2615CE13B0BC97A9627",
|
||||
"DefinitionHash_history": [],
|
||||
"CompartmentHash": "ADB65D9D348B899C6EAE6C37033C03ED",
|
||||
"CompartmentHash_history": [],
|
||||
"CompartmentHash_computed": "ADB65D9D348B899C6EAE6C37033C03ED"
|
||||
},
|
||||
"walker_compStructure_SupportTriangle_FrameRight_1x1_epb": {
|
||||
"DefinitionHash": "D728C50F69BA0B9762CD042CEB677F33",
|
||||
"DefinitionHash_history": [],
|
||||
"CompartmentHash": "35D1C1140F34C508C22ABD9B48A4E4E9",
|
||||
"CompartmentHash_history": [],
|
||||
"CompartmentHash_computed": "35D1C1140F34C508C22ABD9B48A4E4E9"
|
||||
},
|
||||
"walker_compWeapon_TurretSlot_OpenCorner_1x1_epb": {
|
||||
"DefinitionHash": "C123A9C416FE5128E4F980850168D6C0",
|
||||
"DefinitionHash_history": [],
|
||||
"CompartmentHash": "DC4A0367251F174A7D16714B2B688BCE",
|
||||
"CompartmentHash_history": [],
|
||||
"CompartmentHash_computed": "DC4A0367251F174A7D16714B2B688BCE"
|
||||
},
|
||||
"walker_compWeapon_TurretSlot_Open_1x1_epb": {
|
||||
"DefinitionHash": "A9137E71C611F618F49754C6B3B52651",
|
||||
"DefinitionHash_history": [],
|
||||
"CompartmentHash": "6874082929CAFFC20066A8C76D4B32C9",
|
||||
"CompartmentHash_history": [],
|
||||
"CompartmentHash_computed": "6874082929CAFFC20066A8C76D4B32C9"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,137 +0,0 @@
|
||||
{
|
||||
"_source": "islands_assets_all.bundle island_* prefab -> child Landmark (LandmarkBehaviour.name) -> i2 Toponyms/<name>",
|
||||
"islands": {
|
||||
"island_Achilleon": {
|
||||
"toponym": "Achilleon",
|
||||
"display_en": null
|
||||
},
|
||||
"island_Archipel": {
|
||||
"toponym": "Archipel von Leopold",
|
||||
"display_en": null
|
||||
},
|
||||
"island_Demo_Fort": {
|
||||
"toponym": "Fort",
|
||||
"display_en": null
|
||||
},
|
||||
"island_Demo_KleinesHaus": {
|
||||
"toponym": "Eckzahn",
|
||||
"display_en": null
|
||||
},
|
||||
"island_Demo_Lanzelin": {
|
||||
"toponym": "Lanzelin",
|
||||
"display_en": null
|
||||
},
|
||||
"island_Demo_Marktinsel": {
|
||||
"toponym": "Segen",
|
||||
"display_en": null
|
||||
},
|
||||
"island_Demo_Schlangeninsel": {
|
||||
"toponym": "Schlangeninsel",
|
||||
"display_en": null
|
||||
},
|
||||
"island_Demo_Tieftauchparadies": {
|
||||
"toponym": "Felsig",
|
||||
"display_en": null
|
||||
},
|
||||
"island_Demo_Windig": {
|
||||
"toponym": "Windig",
|
||||
"display_en": null
|
||||
},
|
||||
"island_Demo_Wunderinsel": {
|
||||
"toponym": "Strudel",
|
||||
"display_en": null
|
||||
},
|
||||
"island_DeusExMashineSmall": {
|
||||
"toponym": "Rauchwolke",
|
||||
"display_en": null
|
||||
},
|
||||
"island_Factorio": {
|
||||
"toponym": "Sprengstofffabrik",
|
||||
"display_en": null
|
||||
},
|
||||
"island_FortArpad": {
|
||||
"toponym": "Fort Arpad",
|
||||
"display_en": null
|
||||
},
|
||||
"island_FortIstria": {
|
||||
"toponym": "Fort Istria",
|
||||
"display_en": null
|
||||
},
|
||||
"island_FortTarnopol": {
|
||||
"toponym": "Fort Tarnopol",
|
||||
"display_en": null
|
||||
},
|
||||
"island_Gartenfreude": {
|
||||
"toponym": "Insel St. Clemens",
|
||||
"display_en": null
|
||||
},
|
||||
"island_Kaiserplatz": {
|
||||
"toponym": "Kaiserplatz",
|
||||
"display_en": null
|
||||
},
|
||||
"island_LittleFactory01": {
|
||||
"toponym": "Fort Istria",
|
||||
"display_en": null
|
||||
},
|
||||
"island_LittleFactory02": {
|
||||
"toponym": "Fort Arpad",
|
||||
"display_en": null
|
||||
},
|
||||
"island_LittleFactory03": {
|
||||
"toponym": "Fort Tarnopol",
|
||||
"display_en": null
|
||||
},
|
||||
"island_LittleFactoryArmory01": {
|
||||
"toponym": "Fort Zimmer",
|
||||
"display_en": null
|
||||
},
|
||||
"island_LittleFactoryArmory02": {
|
||||
"toponym": "Fort Metternich",
|
||||
"display_en": null
|
||||
},
|
||||
"island_LittleFactoryArmory03": {
|
||||
"toponym": "Fort Starhemberg",
|
||||
"display_en": null
|
||||
},
|
||||
"island_MeereSauge": {
|
||||
"toponym": "MeereSauge",
|
||||
"display_en": null
|
||||
},
|
||||
"island_Petro": {
|
||||
"toponym": "Kaiserplatz",
|
||||
"display_en": null
|
||||
},
|
||||
"island_Raumnadel": {
|
||||
"toponym": "Raumnadel",
|
||||
"display_en": null
|
||||
},
|
||||
"island_ScharfeSpitzen": {
|
||||
"toponym": "ScharfeSpitzen",
|
||||
"display_en": null
|
||||
},
|
||||
"island_Schwalbenlnsel": {
|
||||
"toponym": "Schwalbenberg",
|
||||
"display_en": null
|
||||
},
|
||||
"island_StufenInsel": {
|
||||
"toponym": "StufenInsel",
|
||||
"display_en": null
|
||||
},
|
||||
"island_Venedig": {
|
||||
"toponym": "Venedig",
|
||||
"display_en": null
|
||||
},
|
||||
"island_VenedigSmall": {
|
||||
"toponym": "Venedig",
|
||||
"display_en": null
|
||||
},
|
||||
"island_testIsland": {
|
||||
"toponym": null,
|
||||
"display_en": null
|
||||
},
|
||||
"island_testIslandTramplers": {
|
||||
"toponym": null,
|
||||
"display_en": null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
{
|
||||
"_source": "CheatItemDefinitionsData.asset (configuration_assets_all.bundle)",
|
||||
"_note": "Master item list (id -> category) parsed from the dev cheat-item definitions MonoBehaviour.",
|
||||
"_count": 121,
|
||||
"categories": {
|
||||
"WEAPON": [
|
||||
"game_packedAutoTurretT1Container",
|
||||
"game_packedAutoTurretT2Container",
|
||||
"game_packedAutoTurretT3Container",
|
||||
"game_packedAutoTurretT4Accelerating_Container",
|
||||
"game_packedShotgunTurretT1Container",
|
||||
"game_packedShotgunTurretT2Container",
|
||||
"game_packedShotgunTurretT3Container",
|
||||
"game_packedShotgunTurretT4DoubleBarrel_Container",
|
||||
"game_packedTurretT1Container",
|
||||
"game_packedTurretT2Container",
|
||||
"game_packedTurretT3Container",
|
||||
"game_packedTurretT4_DoubleBarrelContainer",
|
||||
"game_packedTurretT4_RailGunContainer",
|
||||
"item_repeaterRifle",
|
||||
"item_repeaterRifleOneShot",
|
||||
"item_repeaterRifleQuickReload",
|
||||
"item_revolverQuickReload",
|
||||
"item_revolverQuickReloadEightShot",
|
||||
"item_revolverSmall",
|
||||
"item_revolverSmall_dusters",
|
||||
"item_rifleRepeaterAperture",
|
||||
"item_rocketLauncher",
|
||||
"item_semiAutomaticPistol",
|
||||
"item_semiAutomaticPistol_decreasedMag",
|
||||
"item_semiAutomaticPistol_increasedMag",
|
||||
"item_shotgun",
|
||||
"item_shotgunHandmade",
|
||||
"item_shotgunHandmade_choke",
|
||||
"item_shotgunTriplet",
|
||||
"item_sniperRifle",
|
||||
"item_sniperRifleDoubleBarrel",
|
||||
"item_sniperRifle_ironSights",
|
||||
"item_sniperRifle_ironSights_silencer",
|
||||
"item_sniperRifle_silencer"
|
||||
],
|
||||
"AMMO": [
|
||||
"item_pistolAmmo",
|
||||
"item_pistolAmmo_Armor",
|
||||
"item_pistolAmmo_Fire",
|
||||
"item_pistolAmmo_Toxic",
|
||||
"item_pistolAmmo_highVelocity",
|
||||
"item_rifleAmmo",
|
||||
"item_rifleAmmo_Armor",
|
||||
"item_rifleAmmo_Fire",
|
||||
"item_rifleAmmo_Toxic",
|
||||
"item_rifleAmmo_highVelocity",
|
||||
"item_rocketLauncherAmmoArmorPiercing",
|
||||
"item_rocketLauncherAmmoHighExplosion",
|
||||
"item_shotgunAmmo",
|
||||
"item_shotgunAmmo_Armor",
|
||||
"item_shotgunAmmo_Fire",
|
||||
"item_shotgunAmmo_Toxic",
|
||||
"item_shotgunAmmo_explosive",
|
||||
"item_shotgunAmmo_slug",
|
||||
"item_shotgunTurretAmmo_slug",
|
||||
"item_smallCannonAmmo_longRange",
|
||||
"item_sniperRifleAmmo",
|
||||
"item_sniperRifleAmmo_highPenetration",
|
||||
"item_turretAmmo_highVelocity"
|
||||
],
|
||||
"RESOURCE_T2": [
|
||||
"item_resourceCoralDust",
|
||||
"item_resourceFabric",
|
||||
"item_resourceGunpowder",
|
||||
"item_resourceLeviathanMeat",
|
||||
"item_resourceLeviathanSkin",
|
||||
"item_resourceMetalRods",
|
||||
"item_resourceMetal_t2",
|
||||
"item_resourceMixtures",
|
||||
"item_resourceWeaponParts"
|
||||
],
|
||||
"LARGE_VALUABLE": [
|
||||
"game_alarmLockBoxBlack",
|
||||
"game_alarmLockBoxGreen",
|
||||
"game_alarmLockBoxYellow",
|
||||
"item_blackBox",
|
||||
"item_cannedFish",
|
||||
"item_crystalHandles",
|
||||
"item_documentSafe",
|
||||
"item_ficus",
|
||||
"item_wineBox"
|
||||
],
|
||||
"KEY": [
|
||||
"game_keyIslandDoorBlack",
|
||||
"game_keyIslandDoorBlue",
|
||||
"game_keyIslandDoorFort",
|
||||
"game_keyIslandDoorGreen",
|
||||
"game_keyIslandDoorRed",
|
||||
"game_keyIslandDoorWhite",
|
||||
"game_keyLockedBox"
|
||||
],
|
||||
"RESOURCE_T1": [
|
||||
"item_resourceCoralPiece",
|
||||
"item_resourceFabricScraps",
|
||||
"item_resourceMetalParts",
|
||||
"item_resourceMetal_t1",
|
||||
"item_resourceScrappedAmmo",
|
||||
"item_resourceThreads"
|
||||
],
|
||||
"ATTACK_CONSUMABLE": [
|
||||
"item_grenadeContact",
|
||||
"item_projectileAmplifier",
|
||||
"item_projectileDeflectShield",
|
||||
"item_projectileSphereShield",
|
||||
"item_smokeGrenade"
|
||||
],
|
||||
"UTILITY_CONSUMABLE": [
|
||||
"MedKit",
|
||||
"RepairKit",
|
||||
"item_healthEmitter",
|
||||
"item_treasureShovel"
|
||||
],
|
||||
"ENERGY": [
|
||||
"item_boostedEnergyBar",
|
||||
"item_boostedEnergyBarV1",
|
||||
"item_energyBar",
|
||||
"item_smokelessEnergyBar"
|
||||
],
|
||||
"RESOURCE_T3": [
|
||||
"item_resourceHighGradeGunpowder",
|
||||
"item_resourceMetal_t3",
|
||||
"item_resourceOpticLenses",
|
||||
"item_resourceReinforcedLeatherStrips"
|
||||
],
|
||||
"ARMOR": [
|
||||
"Old_Jacket",
|
||||
"Old_JacketT2",
|
||||
"Old_JacketT3"
|
||||
],
|
||||
"RAID_EXPLOSIVES": [
|
||||
"item_c4Dynamite",
|
||||
"item_explosiveBig",
|
||||
"item_wokBomb"
|
||||
],
|
||||
"TURRET_AMMO": [
|
||||
"item_shotgunTurretAmmo",
|
||||
"item_smallCannonAmmo",
|
||||
"item_turretAmmo"
|
||||
],
|
||||
"SMALL_VALUABLE": [
|
||||
"item_smallValuables",
|
||||
"item_valuablePapers"
|
||||
],
|
||||
"WEAPON_BELT": [
|
||||
"item_antiReactorGun",
|
||||
"item_orbitalStrikeGun"
|
||||
],
|
||||
"BACKPACK": [
|
||||
"item_backpack01"
|
||||
],
|
||||
"FOOD": [
|
||||
"item_foodCan"
|
||||
],
|
||||
"MONEY": [
|
||||
"item_coinCrown"
|
||||
]
|
||||
},
|
||||
"item_to_category": {
|
||||
"MedKit": "UTILITY_CONSUMABLE",
|
||||
"Old_Jacket": "ARMOR",
|
||||
"Old_JacketT2": "ARMOR",
|
||||
"Old_JacketT3": "ARMOR",
|
||||
"RepairKit": "UTILITY_CONSUMABLE",
|
||||
"game_alarmLockBoxBlack": "LARGE_VALUABLE",
|
||||
"game_alarmLockBoxGreen": "LARGE_VALUABLE",
|
||||
"game_alarmLockBoxYellow": "LARGE_VALUABLE",
|
||||
"game_keyIslandDoorBlack": "KEY",
|
||||
"game_keyIslandDoorBlue": "KEY",
|
||||
"game_keyIslandDoorFort": "KEY",
|
||||
"game_keyIslandDoorGreen": "KEY",
|
||||
"game_keyIslandDoorRed": "KEY",
|
||||
"game_keyIslandDoorWhite": "KEY",
|
||||
"game_keyLockedBox": "KEY",
|
||||
"game_packedAutoTurretT1Container": "WEAPON",
|
||||
"game_packedAutoTurretT2Container": "WEAPON",
|
||||
"game_packedAutoTurretT3Container": "WEAPON",
|
||||
"game_packedAutoTurretT4Accelerating_Container": "WEAPON",
|
||||
"game_packedShotgunTurretT1Container": "WEAPON",
|
||||
"game_packedShotgunTurretT2Container": "WEAPON",
|
||||
"game_packedShotgunTurretT3Container": "WEAPON",
|
||||
"game_packedShotgunTurretT4DoubleBarrel_Container": "WEAPON",
|
||||
"game_packedTurretT1Container": "WEAPON",
|
||||
"game_packedTurretT2Container": "WEAPON",
|
||||
"game_packedTurretT3Container": "WEAPON",
|
||||
"game_packedTurretT4_DoubleBarrelContainer": "WEAPON",
|
||||
"game_packedTurretT4_RailGunContainer": "WEAPON",
|
||||
"item_antiReactorGun": "WEAPON_BELT",
|
||||
"item_backpack01": "BACKPACK",
|
||||
"item_blackBox": "LARGE_VALUABLE",
|
||||
"item_boostedEnergyBar": "ENERGY",
|
||||
"item_boostedEnergyBarV1": "ENERGY",
|
||||
"item_c4Dynamite": "RAID_EXPLOSIVES",
|
||||
"item_cannedFish": "LARGE_VALUABLE",
|
||||
"item_coinCrown": "MONEY",
|
||||
"item_crystalHandles": "LARGE_VALUABLE",
|
||||
"item_documentSafe": "LARGE_VALUABLE",
|
||||
"item_energyBar": "ENERGY",
|
||||
"item_explosiveBig": "RAID_EXPLOSIVES",
|
||||
"item_ficus": "LARGE_VALUABLE",
|
||||
"item_foodCan": "FOOD",
|
||||
"item_grenadeContact": "ATTACK_CONSUMABLE",
|
||||
"item_healthEmitter": "UTILITY_CONSUMABLE",
|
||||
"item_orbitalStrikeGun": "WEAPON_BELT",
|
||||
"item_pistolAmmo": "AMMO",
|
||||
"item_pistolAmmo_Armor": "AMMO",
|
||||
"item_pistolAmmo_Fire": "AMMO",
|
||||
"item_pistolAmmo_Toxic": "AMMO",
|
||||
"item_pistolAmmo_highVelocity": "AMMO",
|
||||
"item_projectileAmplifier": "ATTACK_CONSUMABLE",
|
||||
"item_projectileDeflectShield": "ATTACK_CONSUMABLE",
|
||||
"item_projectileSphereShield": "ATTACK_CONSUMABLE",
|
||||
"item_repeaterRifle": "WEAPON",
|
||||
"item_repeaterRifleOneShot": "WEAPON",
|
||||
"item_repeaterRifleQuickReload": "WEAPON",
|
||||
"item_resourceCoralDust": "RESOURCE_T2",
|
||||
"item_resourceCoralPiece": "RESOURCE_T1",
|
||||
"item_resourceFabric": "RESOURCE_T2",
|
||||
"item_resourceFabricScraps": "RESOURCE_T1",
|
||||
"item_resourceGunpowder": "RESOURCE_T2",
|
||||
"item_resourceHighGradeGunpowder": "RESOURCE_T3",
|
||||
"item_resourceLeviathanMeat": "RESOURCE_T2",
|
||||
"item_resourceLeviathanSkin": "RESOURCE_T2",
|
||||
"item_resourceMetalParts": "RESOURCE_T1",
|
||||
"item_resourceMetalRods": "RESOURCE_T2",
|
||||
"item_resourceMetal_t1": "RESOURCE_T1",
|
||||
"item_resourceMetal_t2": "RESOURCE_T2",
|
||||
"item_resourceMetal_t3": "RESOURCE_T3",
|
||||
"item_resourceMixtures": "RESOURCE_T2",
|
||||
"item_resourceOpticLenses": "RESOURCE_T3",
|
||||
"item_resourceReinforcedLeatherStrips": "RESOURCE_T3",
|
||||
"item_resourceScrappedAmmo": "RESOURCE_T1",
|
||||
"item_resourceThreads": "RESOURCE_T1",
|
||||
"item_resourceWeaponParts": "RESOURCE_T2",
|
||||
"item_revolverQuickReload": "WEAPON",
|
||||
"item_revolverQuickReloadEightShot": "WEAPON",
|
||||
"item_revolverSmall": "WEAPON",
|
||||
"item_revolverSmall_dusters": "WEAPON",
|
||||
"item_rifleAmmo": "AMMO",
|
||||
"item_rifleAmmo_Armor": "AMMO",
|
||||
"item_rifleAmmo_Fire": "AMMO",
|
||||
"item_rifleAmmo_Toxic": "AMMO",
|
||||
"item_rifleAmmo_highVelocity": "AMMO",
|
||||
"item_rifleRepeaterAperture": "WEAPON",
|
||||
"item_rocketLauncher": "WEAPON",
|
||||
"item_rocketLauncherAmmoArmorPiercing": "AMMO",
|
||||
"item_rocketLauncherAmmoHighExplosion": "AMMO",
|
||||
"item_semiAutomaticPistol": "WEAPON",
|
||||
"item_semiAutomaticPistol_decreasedMag": "WEAPON",
|
||||
"item_semiAutomaticPistol_increasedMag": "WEAPON",
|
||||
"item_shotgun": "WEAPON",
|
||||
"item_shotgunAmmo": "AMMO",
|
||||
"item_shotgunAmmo_Armor": "AMMO",
|
||||
"item_shotgunAmmo_Fire": "AMMO",
|
||||
"item_shotgunAmmo_Toxic": "AMMO",
|
||||
"item_shotgunAmmo_explosive": "AMMO",
|
||||
"item_shotgunAmmo_slug": "AMMO",
|
||||
"item_shotgunHandmade": "WEAPON",
|
||||
"item_shotgunHandmade_choke": "WEAPON",
|
||||
"item_shotgunTriplet": "WEAPON",
|
||||
"item_shotgunTurretAmmo": "TURRET_AMMO",
|
||||
"item_shotgunTurretAmmo_slug": "AMMO",
|
||||
"item_smallCannonAmmo": "TURRET_AMMO",
|
||||
"item_smallCannonAmmo_longRange": "AMMO",
|
||||
"item_smallValuables": "SMALL_VALUABLE",
|
||||
"item_smokeGrenade": "ATTACK_CONSUMABLE",
|
||||
"item_smokelessEnergyBar": "ENERGY",
|
||||
"item_sniperRifle": "WEAPON",
|
||||
"item_sniperRifleAmmo": "AMMO",
|
||||
"item_sniperRifleAmmo_highPenetration": "AMMO",
|
||||
"item_sniperRifleDoubleBarrel": "WEAPON",
|
||||
"item_sniperRifle_ironSights": "WEAPON",
|
||||
"item_sniperRifle_ironSights_silencer": "WEAPON",
|
||||
"item_sniperRifle_silencer": "WEAPON",
|
||||
"item_treasureShovel": "UTILITY_CONSUMABLE",
|
||||
"item_turretAmmo": "TURRET_AMMO",
|
||||
"item_turretAmmo_highVelocity": "AMMO",
|
||||
"item_valuablePapers": "SMALL_VALUABLE",
|
||||
"item_wineBox": "LARGE_VALUABLE",
|
||||
"item_wokBomb": "RAID_EXPLOSIVES"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,853 +0,0 @@
|
||||
{
|
||||
"_source": "CheatItemDefinitionsData.Items (List<ItemDefinition>) — the authoritative item registry; names/descriptions from I2 localization. An entry exists iff the game defines it as a carriable item (has an ItemDefinition with a StorageStack). Damage-type name variants (_Ranged/_Melee) and world objects are intentionally excluded.",
|
||||
"_count": 121,
|
||||
"items": {
|
||||
"item_sniperRifleAmmo": {
|
||||
"name": "11x54 mm Ammo",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 11 mm\nSuitable for: Heavy rifles",
|
||||
"description": "Basic ammo used by long-range rifles. Manufactured by SGOW."
|
||||
},
|
||||
"item_sniperRifleAmmo_highPenetration": {
|
||||
"name": "11x54 mm AP Ammo",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 11 mm\nSuitable for: Heavy rifles\nFeatures: Armor-piercing",
|
||||
"description": "Special armor-piercing ammo with a hardened penetrating core. Used with long-range rifles. Manufactured by SGOW."
|
||||
},
|
||||
"item_shotgunAmmo": {
|
||||
"name": "12 GA Ammo",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 12 GA\nSuitable for: Shotguns",
|
||||
"description": "Basic buckshot ammo used by all shotguns. Manufactured by SGOW."
|
||||
},
|
||||
"item_shotgunAmmo_explosive": {
|
||||
"name": "12 GA Buckshot Ammo (HE)",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 12 GA\nSuitable for: Shotguns\nFeatures: Explodes on impact",
|
||||
"description": "Explosive buckshot ammo. Buckshot pellets are filled with an explosive mixture. Extraordinary efforts led to an impressive result. Manufactured by Kaiser's Friends."
|
||||
},
|
||||
"item_shotgunAmmo_Fire": {
|
||||
"name": "12 GA Dragon's Breath Ammo",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 12 GA\nSuitable for: Shotguns\nFeatures: Incendiary",
|
||||
"description": "Incendiary buckshot ammo. Buckshot pellets are mixed with highly flammable materials. A prototype made by Neue Zukunft."
|
||||
},
|
||||
"item_shotgunAmmo_Armor": {
|
||||
"name": "12 GA Heavy Buckshot Ammo",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 12 GA\nSuitable for: Shotguns\nFeatures: Armor-piercing",
|
||||
"description": "Special buckshot ammo used by all shotguns. Bigger pellets make it more effective against armor. Manufactured by SGOW."
|
||||
},
|
||||
"item_shotgunAmmo_slug": {
|
||||
"name": "12 GA Shotgun Slug",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 12 GA\nSuitable for: Shotguns\nFeatures: Single slug",
|
||||
"description": "Experimental ammunition that uses a single solid bullet instead of buckshot, which significantly increases lethality. Designed for hunting very large animals. Manufactured by SGOW."
|
||||
},
|
||||
"item_shotgunAmmo_Toxic": {
|
||||
"name": "12 GA Toxic Ammo",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 12 GA\nSuitable for: Shotguns\nFeatures: Toxic",
|
||||
"description": "Special buckshot ammo. Buckshot pellets are mixed with highly toxic materials provided by Sophie's Molfars."
|
||||
},
|
||||
"item_shotgunTurretAmmo_slug": {
|
||||
"name": "70 mm Shotgun Cannon Slug",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Shells\nCaliber: 70 mm\nSuitable for: Shotgun cannons\nFeatures: Single slug",
|
||||
"description": "Armor-piercing ammunition with a solid shell, designed for use with 70 mm shotgun cannons. It can't compete with 80 mm long-barreled artillery shells, but without those – beggars can't be choosers."
|
||||
},
|
||||
"item_pistolAmmo": {
|
||||
"name": "8x21 mm Ammo",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 8 mm\nSuitable for: Pistols and revolvers",
|
||||
"description": "Basic ammo used by most small firearms. Manufactured by SGOW."
|
||||
},
|
||||
"item_pistolAmmo_Armor": {
|
||||
"name": "8x21 mm FMJ Ammo",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 8 mm\nSuitable for: Pistols and revolvers\nFeatures: Armor-piercing",
|
||||
"description": "Special ammo used by most small firearms. Its full metal jacket makes it highly effective against armor. Manufactured by SGOW."
|
||||
},
|
||||
"item_pistolAmmo_highVelocity": {
|
||||
"name": "8x21 mm HV Ammo",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 8 mm\nSuitable for: Pistols and revolvers\nFeatures: Increased velocity",
|
||||
"description": "Special high-velocity ammo used by most small firearms. Its increased powder charge makes bullets fly faster compared to standard ammunition. Manufactured by SGOW."
|
||||
},
|
||||
"item_pistolAmmo_Fire": {
|
||||
"name": "8x21 mm Incendiary Ammo",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 8 mm\nSuitable for: Pistols and revolvers\nFeatures: Incendiary",
|
||||
"description": "Special ammo used by most small firearms. Contains highly flammable materials inside. Manufactured by SGOW."
|
||||
},
|
||||
"item_pistolAmmo_Toxic": {
|
||||
"name": "8x21 mm Toxic Ammo",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 8 mm\nSuitable for: Pistols and revolvers\nFeatures: Toxic",
|
||||
"description": "Special ammo used by most small firearms. Contains highly toxic materials provided by Sophie's Molfars."
|
||||
},
|
||||
"item_rifleAmmo": {
|
||||
"name": "9x42 mm Ammo",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 9 mm\nSuitable for: Rifles",
|
||||
"description": "Ammo that is used by most infantry rifles. Manufactured by SGOW."
|
||||
},
|
||||
"item_rifleAmmo_Armor": {
|
||||
"name": "9x42 mm FMJ Ammo",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 9 mm\nSuitable for: Rifles\nFeatures: Armor-piercing",
|
||||
"description": "Special ammo for basic infantry rifles. Its full metal jacket makes it highly effective against armor. Manufactured by SGOW."
|
||||
},
|
||||
"item_rifleAmmo_highVelocity": {
|
||||
"name": "9x42 mm HV Ammo",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 9 mm\nSuitable for: Rifles\nFeatures: Increased velocity",
|
||||
"description": "Special high-velocity ammo for basic infantry rifles. Increased powder charge makes bullets fly faster compared to standard ammunition. Manufactured by SGOW."
|
||||
},
|
||||
"item_rifleAmmo_Fire": {
|
||||
"name": "9x42 mm Incendiary Ammo",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 9 mm\nSuitable for: Rifles\nFeatures: Incendiary",
|
||||
"description": "Special ammo for basic infantry rifles. Contains highly flammable materials inside. Manufactured by SGOW."
|
||||
},
|
||||
"item_rifleAmmo_Toxic": {
|
||||
"name": "9x42 mm Toxic Ammo",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: 9 mm\nSuitable for: Rifles\nFeatures: Toxic",
|
||||
"description": "Special ammo for basic infantry rifles. Contains highly toxic materials inside. Manufactured by SGOW."
|
||||
},
|
||||
"item_rocketLauncherAmmoArmorPiercing": {
|
||||
"name": "Armor-Piercing Rocket",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: Rocket\nSuitable for: Rocket launchers\nFeatures: Increased damage against tramplers",
|
||||
"description": "An unofficial rocket for the RW1 rocket launcher, handmade by someone very resourceful. Packs a punch, but is ineffective against infantry."
|
||||
},
|
||||
"item_turretAmmo_highVelocity": {
|
||||
"name": "High Velocity 80 mm Shell",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Shells\nCaliber: 80 mm\nSuitable for: Naval cannons\nFeatures: Increased velocity",
|
||||
"description": "A modification of standard artillery shells, compatible with naval cannons. Increased initial projectile velocity, made possible by a revised powder charge formula."
|
||||
},
|
||||
"item_rocketLauncherAmmoHighExplosion": {
|
||||
"name": "High-Explosive Rocket",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Ammo\nCaliber: Rocket\nSuitable for: Rocket Launchers\nFeatures: Increased damage against infantry",
|
||||
"description": "An unofficial rocket for the RW1 rocket launcher, handmade by someone very resourceful. Not effective against armor, but capable of inflicting significant damage to infantry in a fairly large radius."
|
||||
},
|
||||
"item_smallCannonAmmo_longRange": {
|
||||
"name": "Long-Range 40 mm Shell",
|
||||
"type": "AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Shells\nCaliber: 40 mm\nSuitable for: Autocannons\nFeatures: Increased range",
|
||||
"description": "A smaller-caliber cannon shell utilized by autocannons. Contains increased powder charge to increase range."
|
||||
},
|
||||
"Old_Jacket": {
|
||||
"name": "Old Smuggler's Jacket",
|
||||
"type": "ARMOR",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Armor\nProtection: Physical, level 1\nFeatures: Regenerates over time",
|
||||
"description": "The leather is sturdy enough to save you from a shot or two."
|
||||
},
|
||||
"Old_JacketT2": {
|
||||
"name": "SGOW SW.52 Body Armor (decommissioned)",
|
||||
"type": "ARMOR",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Armor\nProtection: Physical, level 2\nFeatures: Regenerates over time",
|
||||
"description": "A well-worn breastplate, quilted jacket, and stylish belt – it immediately becomes clear that the wearer is not only ready for battle, but has already been through many."
|
||||
},
|
||||
"Old_JacketT3": {
|
||||
"name": "SGOW SW.73 Light Armor",
|
||||
"type": "ARMOR",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Armor\nProtection: Physical, level 3\nFeatures: Regenerates over time",
|
||||
"description": "The standard armor of combat sailors and field officers of the Kaiser's fleet. The breastplate can reflect even the sun's rays. In it, you will be the most stylish Galician <s>marauder</s> scientist of all! And definitely the most resilient."
|
||||
},
|
||||
"item_projectileSphereShield": {
|
||||
"name": "Domovyk Protective Dome",
|
||||
"type": "ATTACK_CONSUMABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Experimental Equipment\nPurpose: Defensive\nAction: Stops bullets and shells fired from outside\nFeatures: One-time use",
|
||||
"description": "A portable protection station. When activated, generates a magnetic field designed to slow or even stop any physical objects moving into it at high velocity. Won't stop a human or a trampler, but works well against harsh environmental conditions such as bullets and projectiles. Included in the standard set of pioneer equipment."
|
||||
},
|
||||
"item_grenadeContact": {
|
||||
"name": "HG-6 Contact Grenade",
|
||||
"type": "ATTACK_CONSUMABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Grenade\nFuse: Contact\nDamage: High",
|
||||
"description": "A single-use hand grenade – or a reusable mace. Fragile. Handle with care."
|
||||
},
|
||||
"item_projectileAmplifier": {
|
||||
"name": "Pestkop Lorenz Amplifier",
|
||||
"type": "ATTACK_CONSUMABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Experimental Equipment\nPurpose: Offensive\nAction: Increases the damage of bullets and shells fired from the inside\nFeatures: One-time use",
|
||||
"description": "A portable amplifier station. When activated, it generates an electromagnetic field. Metal objects that have started moving inside the field receive an additional charge, which, for example, significantly increases the damage from bullets and shells. Initially developed to protect against harmful radiation, in practice, it has also proven useful in a somewhat opposite way."
|
||||
},
|
||||
"item_smokeGrenade": {
|
||||
"name": "RG79s Smoke Grenade",
|
||||
"type": "ATTACK_CONSUMABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Grenade\nFuse: Contact\nFeatures: Lots of smoke",
|
||||
"description": "A disposable source of black smoke. Whether it will be a decoy, signal, or disguise is up to you. Handle with care."
|
||||
},
|
||||
"item_projectileDeflectShield": {
|
||||
"name": "Von Liebig Reflector",
|
||||
"type": "ATTACK_CONSUMABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Experimental Equipment\nPurpose: Defensive\nAction: Reflects bullets and shells fired from outside\nFeatures: One-time use",
|
||||
"description": "Deflects projectiles in the direction it was pointed when activated. The result of experiments involving aurogen in the mirror-manufacturing process."
|
||||
},
|
||||
"item_backpack01": {
|
||||
"name": "Backpack",
|
||||
"type": "BACKPACK",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Gear\nPurpose: Extra space to carry things hands-free\nCapacity: X slots",
|
||||
"description": "Your adventure companion: holds everything from guns to treasures!"
|
||||
},
|
||||
"item_boostedEnergyBar": {
|
||||
"name": "NZ Mk1 Modified Energy Rod",
|
||||
"type": "ENERGY",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Fuel\nBase: Aurogen\nPurpose: Increases the reactor's stored energy level\nFeatures: Short-term increase in trampler speed (low)",
|
||||
"description": "A main energy source. A standardized capsule with aurogen that fits most reactors and motor-reactors.\nMakes your trampler go really fast for a short time thanks to the modernization of the valve system, which gives the motor-reactor a short burst of extra energy right after the rod is activated."
|
||||
},
|
||||
"item_energyBar": {
|
||||
"name": "NZ Mk2 Energy Rod",
|
||||
"type": "ENERGY",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Fuel\nBase: Aurogen\nPurpose: Increases the reactor's stored energy level",
|
||||
"description": "Your main source of energy. Put it in your reactor to keep the trampler working."
|
||||
},
|
||||
"item_smokelessEnergyBar": {
|
||||
"name": "NZ Mk2-RF Smokeless Energy Rod",
|
||||
"type": "ENERGY",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Fuel\nBase: Aurogen\nPurpose: Increases the reactor's stored energy level\nFeatures: Hides smoke from the trampler's motor-reactor (temporarily)",
|
||||
"description": "A main energy source. A standardized capsule with aurogen that fits most reactors and motor-reactors.\nA covert ops version that can cause the motor-reactor to stop emitting visible smoke for quite a long period of time."
|
||||
},
|
||||
"item_boostedEnergyBarV1": {
|
||||
"name": "NZ Mk2a Enriched Energy Rod",
|
||||
"type": "ENERGY",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Fuel\nBase: Aurogen\nPurpose: Increases the reactor's stored energy level\nFeatures: Short-term increase in trampler speed (average)",
|
||||
"description": "A main energy source. A standardized capsule with aurogen that fits most reactors and motor-reactors.\nMakes your trampler go pretty fast for a short time thanks to the improved structure of the aurogen, which gives the motor-reactor a short burst of extra energy right after the rod is activated."
|
||||
},
|
||||
"item_foodCan": {
|
||||
"name": "Canned Food",
|
||||
"type": "FOOD",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Food\nFeatures:\n- When consumed, regenerates health;\n- When placed in the trampler's fridge or other storage, allows for quick respawning",
|
||||
"description": "The amount of food on board the trampler determines the number of quick respawns available for the Crew. You also can just eat it to improve your well-being."
|
||||
},
|
||||
"game_keyIslandDoorBlack": {
|
||||
"name": "Black Key",
|
||||
"type": "KEY",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Key\nSuitable for: Black doors\nFeatures: One-time use\n\nThe tag text is decorated with military emblems and states: \"Kaiser Fleet's Flagship, The Great _________, Admiral's Quarters\".",
|
||||
"description": "Opens something marked with black. Looks important.\nThe tag text is decorated with military emblems and states: \"Kaiser Fleet's Flagship, The Great _________, Admiral's Quarters\"."
|
||||
},
|
||||
"game_keyIslandDoorBlue": {
|
||||
"name": "Blue Key",
|
||||
"type": "KEY",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Key\nSuitable for: Blue doors\nFeatures: One-time use\n\nThe barely distinguishable text on the tag states the following: \"Franz's S_o_age _ut, Me_e__s__g_ fi__in_ vi_l__e\"",
|
||||
"description": "Opens something marked with blue, but what exactly?\nThe barely distinguishable text on the tag states the following: \"Franz's S_o_age _ut, Me_e__s__g_ fi__in_ vi_l__e\"."
|
||||
},
|
||||
"game_keyLockedBox": {
|
||||
"name": "Box Key",
|
||||
"type": "KEY",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Key\nSuitable for: Locked boxes and crates\nFeatures: One-time use",
|
||||
"description": "Looks suitable for a small lock, like those used on chests or boxes."
|
||||
},
|
||||
"game_keyIslandDoorFort": {
|
||||
"name": "Fort Storage Room Key",
|
||||
"type": "KEY",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Key\nSuitable for: Storage room doors\nFeatures: One-time use",
|
||||
"description": "All district officers had this master key. Can open storage rooms in almost any fort."
|
||||
},
|
||||
"game_keyIslandDoorGreen": {
|
||||
"name": "Green Key",
|
||||
"type": "KEY",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Key\nSuitable for: Green doors\nFeatures: One-time use\n\nThe barely distinguishable text on the tag states the following: \"Chi_f Dis_r_ct Ac_o_nt_nt's Office, K__s__pl__ isla_d\".",
|
||||
"description": "Opens something marked with green, but what exactly?\nThe barely distinguishable text on the tag states the following: \"Chi_f Dis_r_ct Ac_o_nt_nt's Office, K__s__pl__ isla_d\"."
|
||||
},
|
||||
"game_keyIslandDoorRed": {
|
||||
"name": "Red Key",
|
||||
"type": "KEY",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Key\nSuitable for: Red doors\nFeatures: One-time use\n\nThe barely distinguishable text on the tag states the following: \"C__ef I__er_al M__it_ry A_chi__ct's Offi_e, S___n isl__d\".",
|
||||
"description": "Opens something marked with red, but what exactly?\nThe barely distinguishable text on the tag states the following: \"C__ef I__er_al M__it_ry A_chi__ct's Offi_e, S___n isl__d\"."
|
||||
},
|
||||
"game_keyIslandDoorWhite": {
|
||||
"name": "White Key",
|
||||
"type": "KEY",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Key\nSuitable for: White doors\nFeatures: One-time use\n\nThe barely distinguishable text on the tag states the following: \"F__t I__ri_ Dis_ri_t Of__e_'s Wor__h_p\".",
|
||||
"description": "Opens something marked with white, but what exactly?\nThe barely distinguishable text on the tag states the following: \"F__t I__ri_ Dis_ri_t Of__e_'s Wor___o_p\"."
|
||||
},
|
||||
"item_blackBox": {
|
||||
"name": "Black Box",
|
||||
"type": "LARGE_VALUABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Trip Data Recorder\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "Incredibly valuable proof of your victory over someone."
|
||||
},
|
||||
"game_alarmLockBoxYellow": {
|
||||
"name": "Box with Radio Beacon",
|
||||
"type": "LARGE_VALUABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Crate\nPurpose: May contain lots of useful things\nFeatures: Requires a key; equipped with an alarm system",
|
||||
"description": "This container is valuable!"
|
||||
},
|
||||
"game_alarmLockBoxGreen": {
|
||||
"name": "Box with Radio Beacon",
|
||||
"type": "LARGE_VALUABLE",
|
||||
"storageStack": 15,
|
||||
"shortDescription": "Type: Crate\nPurpose: May contain lots of useful things\nFeatures: Requires a key; equipped with alarm system",
|
||||
"description": "This container is valuable!"
|
||||
},
|
||||
"game_alarmLockBoxBlack": {
|
||||
"name": "Box with Radio Beacon",
|
||||
"type": "LARGE_VALUABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Crate\nPurpose: May contain lots of useful things\nFeatures: Requires a key; equipped with an alarm system",
|
||||
"description": "This container is valuable!"
|
||||
},
|
||||
"item_cannedFish": {
|
||||
"name": "Canned Sea Deer XL",
|
||||
"type": "LARGE_VALUABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Valuables\nPurpose: Trading, Tech Tree",
|
||||
"description": "An XL-sized can of the famous Sea Deer, an exotic fish discovered by Sophie's first colonists."
|
||||
},
|
||||
"item_wineBox": {
|
||||
"name": "Crate of 1889 Chardonnay",
|
||||
"type": "LARGE_VALUABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Valuables\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "Wine from Sophie's best vineyards. It is well bought by snobs in Vienna."
|
||||
},
|
||||
"item_documentSafe": {
|
||||
"name": "District Officer's Portable Safe",
|
||||
"type": "LARGE_VALUABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Valuables\nPurpose: Trading, Tech Tree",
|
||||
"description": "A safe that contains something very valuable that belonged to the District Officer. Considering how bureaucratic the Austro-Hungarians are, it's probably a bunch of documents."
|
||||
},
|
||||
"item_ficus": {
|
||||
"name": "Ficus",
|
||||
"type": "LARGE_VALUABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Valuables\nPurpose: Trading and Tech Tree\nFeatures: Does not fit on cargo compartment shelves. Evacuation to orbit is possible only while held in-hand. Very delicate and fragile.",
|
||||
"description": "It's a living plant!"
|
||||
},
|
||||
"item_crystalHandles": {
|
||||
"name": "Raw Aurogen Crystal",
|
||||
"type": "LARGE_VALUABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Valuables\nPurpose: Crafting, trading, Tech Tree\n\nFeatures: Hazardous radiation",
|
||||
"description": "A fragment of a strange crystal, ready for transport. Caution – radiation hazard."
|
||||
},
|
||||
"item_coinCrown": {
|
||||
"name": "Crowns",
|
||||
"type": "MONEY",
|
||||
"storageStack": 1000000,
|
||||
"shortDescription": "Type: Money\nPurpose: Crafting, trading, Tech Tree, tramplers",
|
||||
"description": "The main currency here. Has a familiar face on it."
|
||||
},
|
||||
"item_c4Dynamite": {
|
||||
"name": "Time Bomb",
|
||||
"type": "RAID_EXPLOSIVES",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Explosives\nFuse: Clockwork\nDamage: Moderate",
|
||||
"description": "An incredibly serious argument that can open a lot of doors for you. Make sure to activate it before running away."
|
||||
},
|
||||
"item_explosiveBig": {
|
||||
"name": "Wok Bomb",
|
||||
"type": "RAID_EXPLOSIVES",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Explosives\nFuse: Clockwork\nDamage: Absolute",
|
||||
"description": "A sea mine cooked up according to a secret Molfar recipe, with the addition of local coral dust, a pinch of aurogen, and a countdown clock. Served inactivated. Once activated, an explosion is inevitable."
|
||||
},
|
||||
"item_wokBomb": {
|
||||
"name": "Wok Bomb",
|
||||
"type": "RAID_EXPLOSIVES",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Explosives\nFuse: Clockwork\nDamage: Absolute",
|
||||
"description": "A sea mine cooked up according to a secret Molfar recipe, with the addition of local coral dust, a pinch of aurogen, and a countdown clock. Served inactivated. Once activated, an explosion is inevitable. Extremely powerful!"
|
||||
},
|
||||
"item_resourceCoralPiece": {
|
||||
"name": "Coral Chunk",
|
||||
"type": "RESOURCE_T1",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Raw Material\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "One of the most common souvenirs brought by tourists from Sophie, it is a fairly valuable resource required in the production of some items."
|
||||
},
|
||||
"item_resourceFabricScraps": {
|
||||
"name": "Fabric Scraps",
|
||||
"type": "RESOURCE_T1",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Raw Material\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "Various pieces of fabric and leather, mostly remnants of clothing, tents, carpets, curtains and even sails, left long ago in the harsh desert climate. However, some look quite new."
|
||||
},
|
||||
"item_resourceMetal_t1": {
|
||||
"name": "Mechanical Parts",
|
||||
"type": "RESOURCE_T1",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Spare Parts\nPurpose: Crafting, trading, Tech Tree, tramplers",
|
||||
"description": "Commonly used parts in heavy-duty machinery."
|
||||
},
|
||||
"item_resourceMetalParts": {
|
||||
"name": "Scrap Metal",
|
||||
"type": "RESOURCE_T1",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Raw Material\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "Various parts – anything made from metal – gathered into one pile."
|
||||
},
|
||||
"item_resourceScrappedAmmo": {
|
||||
"name": "Scrapped Ammo",
|
||||
"type": "RESOURCE_T1",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Raw Material\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "This ammo will likely blow up in your hands or won't work at all if you try to use it. But the materials from it can still be salvaged and repurposed."
|
||||
},
|
||||
"item_resourceThreads": {
|
||||
"name": "Threads",
|
||||
"type": "RESOURCE_T1",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Raw Material\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "A basic spool of thread used by anyone."
|
||||
},
|
||||
"item_resourceCoralDust": {
|
||||
"name": "Coral Dust",
|
||||
"type": "RESOURCE_T2",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Raw Material\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "Rare subspecies of local coral, additionally cleaned and ground into powder. Widely used in military and pharmaceutical fields."
|
||||
},
|
||||
"item_resourceFabric": {
|
||||
"name": "Fabric",
|
||||
"type": "RESOURCE_T2",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Raw Material\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "Usually a common material, but not in this case: over the years, most local fabric items have either crumbled to dust or are beyond salvage."
|
||||
},
|
||||
"item_resourceGunpowder": {
|
||||
"name": "Gunpowder",
|
||||
"type": "RESOURCE_T2",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Raw Material\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "Standard military gunpowder. Not of the best quality due to cheap components, nevertheless sufficient for mass production. Used for most conventional military ammunition."
|
||||
},
|
||||
"item_resourceLeviathanMeat": {
|
||||
"name": "Leviathan Meat",
|
||||
"type": "RESOURCE_T2",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Raw Material\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "The lump of meat of a mysterious creature that lurks beneath the sands. But where's the carcass?"
|
||||
},
|
||||
"item_resourceLeviathanSkin": {
|
||||
"name": "Leviathan Skin",
|
||||
"type": "RESOURCE_T2",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Raw Material\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "A piece of skin that, by its texture, is reminiscent of a snake's skin but, by the looks of it, this piece belonged to something way bigger that a snake."
|
||||
},
|
||||
"item_resourceMetalRods": {
|
||||
"name": "Metal Rods",
|
||||
"type": "RESOURCE_T2",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Raw Material\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "High-quality metal forged into rods – ready for use and easy to transport."
|
||||
},
|
||||
"item_resourceMixtures": {
|
||||
"name": "Mixtures",
|
||||
"type": "RESOURCE_T2",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Raw Material\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "Various mixtures, syrups, tinctures and pills that were widely available for practically anyone on Sophie."
|
||||
},
|
||||
"item_resourceMetal_t2": {
|
||||
"name": "Pneumatic Parts",
|
||||
"type": "RESOURCE_T2",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Spare Parts\nPurpose: Crafting, trading, Tech Tree, tramplers",
|
||||
"description": "Crucial elements in more sophisticated machinery."
|
||||
},
|
||||
"item_resourceWeaponParts": {
|
||||
"name": "Weapon Parts",
|
||||
"type": "RESOURCE_T2",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Raw Material\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "Receivers, spare barrels, gas tubes and other replaceable parts of regular military weapons."
|
||||
},
|
||||
"item_resourceMetal_t3": {
|
||||
"name": "Computing Module",
|
||||
"type": "RESOURCE_T3",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Spare Parts\nPurpose: Crafting, trading, Tech Tree, tramplers",
|
||||
"description": "These parts were used in the most technological marvels created by the brightest minds of the Empire."
|
||||
},
|
||||
"item_resourceHighGradeGunpowder": {
|
||||
"name": "High-Grade Gunpowder",
|
||||
"type": "RESOURCE_T3",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Raw Material\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "The new smokeless gunpowder recipe, discovered on Sophie. The Imperial forces combined their regular gunpowder with coral dust, resulting in a powder that lasted longer and produced much less smoke."
|
||||
},
|
||||
"item_resourceOpticLenses": {
|
||||
"name": "Optic Lenses",
|
||||
"type": "RESOURCE_T3",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Raw Material\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "These lenses were used everywhere: from rifle and gun sights to eyeglasses and binoculars. Due to their fragile structure, there are not many left on Sophie, so they are considered very valuable."
|
||||
},
|
||||
"item_resourceReinforcedLeatherStrips": {
|
||||
"name": "Reinforced Leather Strips",
|
||||
"type": "RESOURCE_T3",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Raw Material\nPurpose: Crafting, trading, Tech Tree",
|
||||
"description": "Strips of leather sewn with fabric are a fairly durable material used to make uniforms, shoulder pads, and body armor for imperial troops."
|
||||
},
|
||||
"item_smallValuables": {
|
||||
"name": "Small Valuables",
|
||||
"type": "SMALL_VALUABLE",
|
||||
"storageStack": 1000000,
|
||||
"shortDescription": "Type: Valuables\nPurpose: Trading",
|
||||
"description": "Some valuable knick-knacks left behind by the previous residents during the evacuation."
|
||||
},
|
||||
"item_valuablePapers": {
|
||||
"name": "Valuable Papers",
|
||||
"type": "SMALL_VALUABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Valuables\nPurpose: Trading",
|
||||
"description": "These papers might contain some information that can be sold to someone."
|
||||
},
|
||||
"item_smallCannonAmmo": {
|
||||
"name": "40 mm Shell",
|
||||
"type": "TURRET_AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Shells\nCaliber: 40 mm\nSuitable for: Autocannons",
|
||||
"description": "A smaller-caliber cannon shell utilized by autocannons."
|
||||
},
|
||||
"item_shotgunTurretAmmo": {
|
||||
"name": "70 mm Shell",
|
||||
"type": "TURRET_AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Shells\nCaliber: 70 mm\nSuitable for: Shotgun Cannons",
|
||||
"description": "Canister shot that contains a high number of small projectiles. Utilized by shotgun cannons."
|
||||
},
|
||||
"item_turretAmmo": {
|
||||
"name": "80 mm Shell",
|
||||
"type": "TURRET_AMMO",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Shells\nCaliber: 80 mm\nSuitable for: Naval cannons",
|
||||
"description": "A regular type of shell utilized by Naval Cannons."
|
||||
},
|
||||
"MedKit": {
|
||||
"name": "MedKit",
|
||||
"type": "UTILITY_CONSUMABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Medical Supplies\nPurpose: Quick restoration of health\nFeatures: Allows for full restoration",
|
||||
"description": "Whether it's a critical wound or a small scratch, the MedKit will fix it in no time."
|
||||
},
|
||||
"item_healthEmitter": {
|
||||
"name": "Podorozhnyk Bio-Emitter",
|
||||
"type": "UTILITY_CONSUMABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Experimental Equipment\nPurpose: Defensive\nAction: Regenerates health of all living beings within range\nFeatures: One-time use",
|
||||
"description": "A portable medical station. When activated, generates a healing field capable of mending wounds and restoring health. A prototype. Short-term action. Will not protect against fatal injuries."
|
||||
},
|
||||
"RepairKit": {
|
||||
"name": "Repair Kit",
|
||||
"type": "UTILITY_CONSUMABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "",
|
||||
"description": "If you've noticed that your trampler has more holes than it has walls, this is the right tool to use."
|
||||
},
|
||||
"item_treasureShovel": {
|
||||
"name": "Rusty Shovel",
|
||||
"type": "UTILITY_CONSUMABLE",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Tool\nPurpose: Dig up treasures\nFeatures: One-time use",
|
||||
"description": "This shovel is in terrible condition and literally falls apart in your hands. However, according to the accounting journals, it seems that funds for its repair and maintenance were regularly allocated by the foreman."
|
||||
},
|
||||
"item_sniperRifle_ironSights": {
|
||||
"name": "1874e Petros Rifle",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Heavy Rifle\nCaliber: 11 mm\nMagazine: 1 round\nRange: Very High\nReloading Speed: Low",
|
||||
"description": "The Bartka \"Petros\" 11 mm bolt-action rifle. Simple, reliable, and very accurate. Every shot counts that much more when you only have one shot available."
|
||||
},
|
||||
"item_sniperRifle_ironSights_silencer": {
|
||||
"name": "1874e/sd Petros Rifle (Silenced)",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Heavy Rifle\nCaliber: 11 mm\nMagazine: 1 round\nRange: Very High\nReloading Speed: Low\nFeatures: Silencer",
|
||||
"description": "The Bartka \"Petros\" 11 mm bolt-action rifle. Simple, reliable, and very accurate. Equipped with a silencer. A handy tool to surprise your opponent with."
|
||||
},
|
||||
"item_sniperRifle": {
|
||||
"name": "1874s Petros Sniper Rifle",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Heavy Rifle\nCaliber: 11 mm\nMagazine: 1 round\nRange: Very High\nReloading Speed: Low\nFeatures: Telescopic sight",
|
||||
"description": "The Bartka \"Petros\" 11 mm sniper rifle. Equipped with a telescopic sight. Every shot counts that much more when you only have one shot available."
|
||||
},
|
||||
"item_sniperRifle_silencer": {
|
||||
"name": "1874s/sd Petros Sniper Rifle (Silenced)",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Heavy Rifle\nCaliber: 11 mm\nMagazine: 1 round\nRange: Very High\nReloading Speed: Low\nFeatures: Telescopic sight and silencer",
|
||||
"description": "The Bartka \"Petros\" 11 mm sniper rifle. Equipped with a telescopic sight and a silencer. A handy tool to surprise your opponent with."
|
||||
},
|
||||
"item_sniperRifleDoubleBarrel": {
|
||||
"name": "1887/2 \"The Twin\" Double-Barreled Rifle",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Heavy Rifle\nCaliber: 11 mm\nMagazine: 2 rounds\nRange: Very High\nReloading Speed: Low\nFeatures: Double-barreled",
|
||||
"description": "Bartka's \"The Twin\" 11 mm double-barreled sniper rifle. Equipped with a telescopic sight. It's always nice to have a second chance. Even if it's rarely needed, given the power of each shot."
|
||||
},
|
||||
"item_semiAutomaticPistol": {
|
||||
"name": "Blitz 10R Pistol",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Semi-Automatic Pistol\nCaliber: 8 mm\nMagazine: 10 rounds\nRange: Medium\nReloading speed: High",
|
||||
"description": "The O'Donnel \"Blitz\" 8 mm semi-automatic pistol. Swift and deadly. Highly effective in close quarters. Compensates for its low accuracy with a high fire rate."
|
||||
},
|
||||
"item_semiAutomaticPistol_increasedMag": {
|
||||
"name": "Blitz 15R Pistol",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Semi-Automatic Pistol\nCaliber: 8 mm\nMagazine: 15 rounds\nRange: Medium\nReloading Speed: High",
|
||||
"description": "The O'Donnel \"Blitz\" 8 mm semi-automatic pistol. Swift and deadly. A rare modification with an increased capacity magazine, capable of holding 15 rounds. Even more bullets to miss the target with."
|
||||
},
|
||||
"item_semiAutomaticPistol_decreasedMag": {
|
||||
"name": "Blitz PPS-5 Pistol",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Semi-Automatic Pistol\nCaliber: 8 mm\nMagazine: 5 rounds\nRange: Medium\nReloading Speed: High",
|
||||
"description": "The O'Donnel \"Blitz\" 8 mm semi-automatic pistol. Swift and deadly. A preproduction sample with a reduced capacity magazine capable of holding five rounds."
|
||||
},
|
||||
"item_shotgunHandmade": {
|
||||
"name": "Drobulet Shotgun",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Shotgun\nCaliber: 12 GA\nMagazine: 1 round\nRange: Very Short\nReloading Speed: Medium",
|
||||
"description": "A makeshift 12 GA shotgun frequently used by Kaiser's Friends. They call it a \"Drobulet\" between each other."
|
||||
},
|
||||
"item_shotgunHandmade_choke": {
|
||||
"name": "Drobulet Shotgun (Vertical Choke)",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Shotgun\nCaliber: 12 GA\nMagazine: 1 round\nRange: Very Short\nReloading Speed: Medium\nFeatures: Vertical choke",
|
||||
"description": "A makeshift 12 GA shotgun frequently used by Kaiser's Friends. Additionally equipped with a vertical choke to increase effectiveness against tall and thin targets."
|
||||
},
|
||||
"item_revolverSmall_dusters": {
|
||||
"name": "EB \"Bantam\" Revolver",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Revolver\nCaliber: 8 mm\nMagazine: 6 rounds\nRange: Short\nReloading Speed: Low\nFeatures: Increased melee damage",
|
||||
"description": "A six-shot 8 mm revolver made by Eisenblut, improved by Kaiser's Friends for very close combat encounters."
|
||||
},
|
||||
"item_revolverQuickReloadEightShot": {
|
||||
"name": "EB \"Tiszt\" Award Revolver",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Revolver\nCaliber: 8 mm\nMagazine: 8 rounds\nRange: Short\nReloading Speed: High",
|
||||
"description": "A special modification of the 8 mm revolver manufactured by Eisenblut, used as decoration for personal service to the state, courage and high professionalism. In addition to artistic decoration, the revolver also received an eight-shot drum compatible with speed loading."
|
||||
},
|
||||
"item_revolverQuickReload": {
|
||||
"name": "EB \"Vita\" Police Revolver",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Revolver\nCaliber: 8 mm\nMagazine: 6 rounds\nRange: Short\nReloading Speed: High",
|
||||
"description": "A modified 8 mm revolver manufactured by Eisenblut, used by the Sophie Gendarmerie. Received a six-shot drum compatible with speed loading."
|
||||
},
|
||||
"item_revolverSmall": {
|
||||
"name": "EB \"Zseb\" Revolver",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Revolver\nCaliber: 8 mm\nMagazine: 6 rounds\nRange: Short\nReloading Speed: Low",
|
||||
"description": "One of the cheapest 8 mm revolvers you can find on the grey market. Production – Eisenblut; sales – virtually any arms dealer."
|
||||
},
|
||||
"game_packedShotgunTurretT4DoubleBarrel_Container": {
|
||||
"name": "Experimental 2x70 mm Twin Shotgun Cannon Kit",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Artillery\nCaliber: 70 mm\nRange: Short\n\nState: Perfect\n\nFeatures: Double-barreled",
|
||||
"description": "One gun is already good, but two is always better. An experimental twin 70 mm shotgun cannon, created by SGOW as an attempt to increase firepower and salvo weight without significantly increasing the dimensions of the turret. Named \"Eight\" internally for its specific appearance. Manufactured in a limited batch for field testing.\nA complete set of spare parts and tools for field assembly and disassembly.\n\nIn perfect condition, as if fresh from the factory.\n\nUtilizes 70 mm shells."
|
||||
},
|
||||
"game_packedAutoTurretT4Accelerating_Container": {
|
||||
"name": "Experimental 40 mm Autocannon Kit",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Artillery\nCaliber: 40 mm\nRange: Medium\n\nState: Perfect\n\nFeatures: Heating increases the rate of fire. Overheating can cause it to jam.",
|
||||
"description": "A modified trigger mechanism, experimentation with materials and a little barrel cooling system wizardry, and a regular twin-barreled anti-aircraft gun gained the ability to increase its combat rate of fire from its own heating during firing. Be careful: watch the temperature!\nA complete set of spare parts and tools for field assembly and disassembly.\n\nIn perfect condition, as if fresh from the laboratory.\n\nUtilizes 40 mm shells."
|
||||
},
|
||||
"game_packedTurretT4_RailGunContainer": {
|
||||
"name": "Experimental 80 mm Railgun Kit",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Artillery\nCaliber: 80 mm\nRange: Very High\n\nState: Perfect\n\nFeatures: The extremely high initial velocity of the shells allows for direct fire at any distance.",
|
||||
"description": "A naval cannon, retrofitted in the Neue Zukunft laboratories with an electromagnetic accelerator. It consumes a huge amount of energy, but allows you to forget about ballistics – even standard 80 mm shells receive such a high initial velocity that the cannon literally shoots \"on target\" at any distance.\nA complete set of spare parts and tools for field assembly and disassembly.\n\nIn perfect condition, as if fresh from the laboratory.\n\nUtilizes 80 mm shells."
|
||||
},
|
||||
"game_packedTurretT4_DoubleBarrelContainer": {
|
||||
"name": "Experimental 80mm Cannon Kit",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "",
|
||||
"description": "##Placeholder##"
|
||||
},
|
||||
"item_repeaterRifleOneShot": {
|
||||
"name": "item_repeaterRifleOneShot",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "",
|
||||
"description": ""
|
||||
},
|
||||
"item_repeaterRifleQuickReload": {
|
||||
"name": "item_repeaterRifleQuickReload",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "",
|
||||
"description": ""
|
||||
},
|
||||
"item_rifleRepeaterAperture": {
|
||||
"name": "M82 \"Aperture\" Rifle",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Semi-Automatic Rifle\nCaliber: 9 mm\nMagazine: 6 rounds\nRange: High\nReloading speed: High\nFeatures: Aperture sight",
|
||||
"description": "The SGOW M82 9 mm revolver rifle with a modified aperture sight. Highly versatile like a Swiss Army knife. Quite slow when it comes to reloading. Mounted with the special sights that allow the shooter to cover longer distances with increased accuracy."
|
||||
},
|
||||
"item_repeaterRifle": {
|
||||
"name": "M82 Rifle",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Semi-Automatic Rifle\nCaliber: 9 mm\nMagazine: 6 rounds\nRange: High\nReloading Speed: Medium",
|
||||
"description": "The SGOW M82 9 mm revolver rifle. Highly versatile like a Swiss Army knife. Quite slow when it comes to reloading."
|
||||
},
|
||||
"item_shotgun": {
|
||||
"name": "Pepper Mill Shotgun",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Shotgun\nCaliber: 12 GA\nMagazine: 3 rounds\nRange: Very Short\nReloading Speed: Medium",
|
||||
"description": "The custom O'Donnel 12 GA shotgun that was called the \"Pepper Mill\" because of it rotating barrels. \"Give 'Em Some Pepper!\""
|
||||
},
|
||||
"game_packedAutoTurretT3Container": {
|
||||
"name": "Pristine 40 mm Autocannon Kit",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Artillery\nCaliber: 40 mm\nRange: Medium\n\nState: Pristine\n\nFeatures: May jam when overheated",
|
||||
"description": "Some ingenious engineers at the station decided that this twin-barreled anti-aircraft cannon would work perfectly fine against ground targets as well. Because of the old barrel design, the cannon may overheat and jam if it shoots consecutively for too long.\nA complete set of spare parts and tools for field assembly and disassembly.\n\nIn perfect condition, as if fresh from the factory.\n\nUtilizes 40 mm shells."
|
||||
},
|
||||
"game_packedShotgunTurretT3Container": {
|
||||
"name": "Pristine 70 mm Shotgun Cannon Kit",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Artillery\nCaliber: 70 mm\nRange: Short\n\nState: Pristine",
|
||||
"description": "A cannon designed for buckshot shells that disperse many small projectiles when fired. The armor plate mounted on the front of the turret will protect the cannon operator from most gunfire. It is highly lethal against infantry at close range. The projectiles are solid enough to deal some damage to structures as well.\nA complete set of spare parts and tools for field assembly and disassembly.\n\nIn perfect condition, as if fresh from the factory.\n\nUtilizes 70 mm shells."
|
||||
},
|
||||
"game_packedTurretT3Container": {
|
||||
"name": "Pristine 80 mm Naval Cannon Kit",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Artillery\nCaliber: 80 mm\nRange: High\n\nState: Pristine",
|
||||
"description": "A naval cannon stripped from Sophie's military fleet tramplers by scavengers. Covers great distances but is quite hard to operate due to its slow rate of fire.\nA complete set of spare parts and tools for field assembly and disassembly.\n\nIn perfect condition, as if fresh from the factory.\n\nUtilizes 80 mm shells."
|
||||
},
|
||||
"game_packedAutoTurretT1Container": {
|
||||
"name": "Rusty 40 mm Autocannon Kit",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Artillery\nCaliber: 40 mm\nRange: Medium\n\nState: Rusty\n\nFeatures: May jam when overheated",
|
||||
"description": "Some ingenious engineers at the station decided that this twin-barreled anti-aircraft cannon would work perfectly fine against ground targets as well. Because of the old barrel design, the cannon may overheat and jam if it shoots consecutively for too long.\nA complete set of spare parts and tools for field assembly and disassembly.\n\nDue to long-term storage in adverse conditions, it is slightly rusted, but can still be used for its intended purpose.\n\nUtilizes 40 mm shells."
|
||||
},
|
||||
"game_packedShotgunTurretT1Container": {
|
||||
"name": "Rusty 70 mm Shotgun Cannon Kit",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Artillery\nCaliber: 70 mm\nRange: Short\n\nState: Rusty",
|
||||
"description": "A cannon designed for buckshot shells that disperse many small projectiles when fired. The armor plate mounted on the front of the turret will protect the cannon operator from most gunfire. It is highly lethal against infantry at close range. The projectiles are solid enough to deal some damage to structures as well.\nA complete set of spare parts and tools for field assembly and disassembly.\n\nDue to long-term storage in adverse conditions, it is slightly rusted, but can still be used for its intended purpose.\n\nUtilizes 70 mm shells."
|
||||
},
|
||||
"game_packedTurretT1Container": {
|
||||
"name": "Rusty 80 mm Naval Cannon Kit",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Artillery\nCaliber: 80 mm\nRange: High\n\nState: Rusty",
|
||||
"description": "A naval cannon stripped from Sophie's military fleet tramplers by scavengers. Covers great distances but is quite hard to operate due to its slow rate of fire.\nA complete set of spare parts and tools for field assembly and disassembly.\n\nDue to long-term storage in adverse conditions, it is slightly rusted, but can still be used for its intended purpose.\n\nUtilizes 80 mm shells."
|
||||
},
|
||||
"item_rocketLauncher": {
|
||||
"name": "RW1 Rocket Launcher",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Rocket Launcher\nAmmo: Rockets\nMagazine: 1\nRange: Medium\nReloading Speed: Low",
|
||||
"description": "The Raketenwerfer 1 is one of the secret prototypes built by the Kaiser's SGOW engineers on Sophie. Unfortunately, it never reached Earth -– the entire batch was \"lost\" during the mass evacuation."
|
||||
},
|
||||
"item_shotgunTriplet": {
|
||||
"name": "Triplet Shotgun",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Shotgun\nCaliber: 12 GA\nMagazine: 3 rounds\nRange: Very Short\nReloading Speed: Medium\nFeatures: Volley fire",
|
||||
"description": "O'Donnell's exclusive 12-gauge shotgun, nicknamed the \"Triplet\" because it fires in bursts from all three barrels at once. Blows away everything in its path (literally!)."
|
||||
},
|
||||
"game_packedAutoTurretT2Container": {
|
||||
"name": "Worn 40 mm Autocannon Kit",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Artillery\nCaliber: 40 mm\nRange: Medium\n\nState: Worn\n\nFeatures: May jam when overheated",
|
||||
"description": "Some ingenious engineers at the station decided that this twin-barreled anti-aircraft cannon would work perfectly fine against ground targets as well. Because of the old barrel design, the cannon may overheat and jam if it shoots consecutively for too long.\nA complete set of spare parts and tools for field assembly and disassembly.\n\nHas signs of active use.\n\nUtilizes 40 mm shells."
|
||||
},
|
||||
"game_packedShotgunTurretT2Container": {
|
||||
"name": "Worn 70 mm Shotgun Cannon Kit",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Artillery\nCaliber: 70 mm\nRange: Short\n\nState: Worn",
|
||||
"description": "A cannon designed for buckshot shells that disperse many small projectiles when fired. The armor plate mounted on the front of the turret will protect the cannon operator from most gunfire. It is highly lethal against infantry at close range. The projectiles are solid enough to deal some damage to structures as well.\nA complete set of spare parts and tools for field assembly and disassembly.\n\nHas signs of active use.\n\nUtilizes 70 mm shells."
|
||||
},
|
||||
"game_packedTurretT2Container": {
|
||||
"name": "Worn 80 mm Naval Cannon Kit",
|
||||
"type": "WEAPON",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Artillery\nCaliber: 80 mm\nRange: High\n\nState: Worn",
|
||||
"description": "A naval cannon stripped from Sophie's military fleet tramplers by scavengers. Covers great distances but is quite hard to operate due to its slow rate of fire.\nA complete set of spare parts and tools for field assembly and disassembly.\n\nHas signs of active use.\n\nUtilizes 80 mm shells."
|
||||
},
|
||||
"item_orbitalStrikeGun": {
|
||||
"name": "Orbital Strike Pointer",
|
||||
"type": "WEAPON_BELT",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Experimental Equipment\nPurpose: Offensive\nAction: Calls for an artillery strike from orbit in the specified area\nFeatures: One-time use",
|
||||
"description": "The portable sighting complex \"Zeigefinger\" of the orbital surveillance system \"Bärenjäger\" (aka \"Berehynia\"), developed by Neue Zukunft in cooperation with SGOW to help pioneers explore new worlds. Rumor has it that the system is still present in the orbit of Sophie."
|
||||
},
|
||||
"item_antiReactorGun": {
|
||||
"name": "The Great Silence",
|
||||
"type": "WEAPON_BELT",
|
||||
"storageStack": 100000,
|
||||
"shortDescription": "Type: Experimental Equipment\nPurpose: Anti-reactor rifle\nTrigger: Hold to charge\nAction: Emergency shutdown of reactors, motor-reactors, and any other kind of aurogen-based power plants",
|
||||
"description": "A special tool designed to neutralize aurogen-based energy sources. Developed by Neue Zukunft as some kind of \"fire extinguisher\" capable of initiating an emergency shutdown of aurogen reactors that have gotten out of control. Thanks to the efforts of Kaiser's Friends, it was transformed into a futuristic anti-reactor rifle."
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,260 +0,0 @@
|
||||
{
|
||||
"_source": "epb_assets_all.bundle game_conveyor_*_epb EntityBlueprint (Odin) -> ProductionLineRecipeComponent.recipe (CraftingRecipe)",
|
||||
"_count": 14,
|
||||
"production_lines": {
|
||||
"game_conveyor_40mmT3Cannon": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_alloySteel",
|
||||
"amount": 40
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetal_t2",
|
||||
"amount": 300
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "game_packedAutoTurretT3Container",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 55.0
|
||||
},
|
||||
"game_conveyor_70mmT3Cannon": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_alloySteel",
|
||||
"amount": 40
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetal_t2",
|
||||
"amount": 300
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "game_packedShotgunTurretT3Container",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 55.0
|
||||
},
|
||||
"game_conveyor_80mmT3Cannon": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_alloySteel",
|
||||
"amount": 40
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetal_t2",
|
||||
"amount": 300
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "game_packedTurretT3Container",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 55.0
|
||||
},
|
||||
"game_conveyor_armorPiercingRockets": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceFabric",
|
||||
"amount": 10
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceGunpowder",
|
||||
"amount": 10
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_rocketLauncherAmmoArmorPiercing",
|
||||
"amount": 3
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
"game_conveyor_baseInventoryToInventory": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceMetal_t1",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_c4Dynamite",
|
||||
"amount": 1
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetal_t1",
|
||||
"amount": 2
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
"game_conveyor_baseInventoryToLarge": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceMetal_t1",
|
||||
"amount": 1
|
||||
},
|
||||
{
|
||||
"itemId": "item_c4Dynamite",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_explosiveBig",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 15.0
|
||||
},
|
||||
"game_conveyor_baseLargeToInventory": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_wineBox",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_coinCrown",
|
||||
"amount": 1000
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 15.0
|
||||
},
|
||||
"game_conveyor_baseLargeToLarge": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_wineBox",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_explosiveBig",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 15.0
|
||||
},
|
||||
"game_conveyor_computingModules": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_blackBox",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_resourceMetal_t3",
|
||||
"amount": 10
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 7.0
|
||||
},
|
||||
"game_conveyor_contactGrenades": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceFabric",
|
||||
"amount": 10
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceGunpowder",
|
||||
"amount": 10
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_grenadeContact",
|
||||
"amount": 5
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
"game_conveyor_coralDust": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceCoralPiece",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_resourceCoralDust",
|
||||
"amount": 10
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetal_t1",
|
||||
"amount": 2
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
"game_conveyor_energyRods": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_crystalHandles",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_energyBar",
|
||||
"amount": 10
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 7.0
|
||||
},
|
||||
"game_conveyor_explosiveSmall": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceFabric",
|
||||
"amount": 5
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceGunpowder",
|
||||
"amount": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_c4Dynamite",
|
||||
"amount": 5
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetal_t1",
|
||||
"amount": 2
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 5.0
|
||||
},
|
||||
"game_conveyor_mechanicalParts": {
|
||||
"inputs": [
|
||||
{
|
||||
"itemId": "item_resourceMetalParts",
|
||||
"amount": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"itemId": "item_resourceMetal_t1",
|
||||
"amount": 2
|
||||
},
|
||||
{
|
||||
"itemId": "item_resourceMetal_t1",
|
||||
"amount": 2
|
||||
}
|
||||
],
|
||||
"craftTimeSeconds": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
132
reverse/capture_hosts.py
Normal file
132
reverse/capture_hosts.py
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Quick triage of a SAND network capture: every host the client touched, in order,
|
||||
with the bits we care about highlighted.
|
||||
|
||||
Pulls:
|
||||
- DNS queries (order = startup sequence; flush DNS first for a clean list)
|
||||
- TLS SNI (HTTPS hostnames even when DNS was cached)
|
||||
- TCP/UDP endpoints
|
||||
and flags the three backends we care about:
|
||||
- <titleId>.playfabapi.com -> prints the **PlayFab TitleId** (the one constant the
|
||||
REST scraper needs)
|
||||
- <region>.hologryph.com -> the master-server region (ws://, port 80, cleartext)
|
||||
- sandconfigstorage.blob... -> the anonymous config CDN
|
||||
|
||||
Usage: venv/bin/python reverse/capture_hosts.py <capture.pcapng>
|
||||
"""
|
||||
import sys, re
|
||||
from scapy.all import rdpcap, DNS, DNSQR, DNSRR, IP, IPv6, TCP, UDP, Raw
|
||||
|
||||
|
||||
def tls_sni(b):
|
||||
"""Extract SNI from a TLS ClientHello payload (bytes). None if not a ClientHello."""
|
||||
try:
|
||||
if len(b) < 6 or b[0] != 0x16 or b[5] != 0x01:
|
||||
return None
|
||||
i = 5 + 4 + 2 + 32 # rec hdr + hs hdr + version + random
|
||||
i += 1 + b[i] # session id
|
||||
i += 2 + int.from_bytes(b[i:i + 2], "big") # cipher suites
|
||||
i += 1 + b[i] # compression methods
|
||||
end = i + 2 + int.from_bytes(b[i:i + 2], "big")
|
||||
i += 2
|
||||
while i + 4 <= end:
|
||||
et = int.from_bytes(b[i:i + 2], "big")
|
||||
el = int.from_bytes(b[i + 2:i + 4], "big")
|
||||
i += 4
|
||||
if et == 0: # server_name
|
||||
j = i + 2
|
||||
nl = int.from_bytes(b[j + 1:j + 3], "big")
|
||||
return b[j + 3:j + 3 + nl].decode(errors="replace")
|
||||
i += el
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
PLAYFAB = re.compile(r"^([0-9A-Fa-f]{4,7})\.playfabapi\.com$")
|
||||
HOLOGRYPH = re.compile(r"^([a-z0-9-]+)\.hologryph\.com$", re.I)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit("usage: capture_hosts.py <pcap>")
|
||||
pk = rdpcap(sys.argv[1])
|
||||
t0 = float(pk[0].time)
|
||||
|
||||
dns_order, dns_seen = [], set()
|
||||
ip2host, snis = {}, {}
|
||||
tcp_first, udp_first = {}, {}
|
||||
|
||||
for p in pk:
|
||||
if p.haslayer(DNS):
|
||||
d = p[DNS]
|
||||
try:
|
||||
qn = p[DNSQR].qname.decode(errors="replace").rstrip(".")
|
||||
except Exception:
|
||||
qn = None
|
||||
if qn and d.qr == 0 and not qn.endswith(".local") and qn != "wpad.localdomain":
|
||||
if qn not in dns_seen:
|
||||
dns_seen.add(qn)
|
||||
dns_order.append((float(p.time) - t0, qn))
|
||||
if qn and d.qr == 1 and d.ancount:
|
||||
for k in range(d.ancount):
|
||||
rr = d.an[k]
|
||||
if rr.type in (1, 28):
|
||||
try:
|
||||
ip2host[str(rr.rdata)] = qn
|
||||
except Exception:
|
||||
pass
|
||||
ipl = p[IP] if p.haslayer(IP) else (p[IPv6] if p.haslayer(IPv6) else None)
|
||||
if ipl is None:
|
||||
continue
|
||||
if p.haslayer(TCP):
|
||||
t = p[TCP]
|
||||
if t.flags & 0x02 and not t.flags & 0x10:
|
||||
tcp_first.setdefault((ipl.dst, t.dport), float(p.time) - t0)
|
||||
if p.haslayer(Raw) and t.dport == 443:
|
||||
s = tls_sni(bytes(p[Raw].load))
|
||||
if s:
|
||||
snis[ipl.dst] = s
|
||||
elif p.haslayer(UDP):
|
||||
u = p[UDP]
|
||||
if u.dport not in (53, 5353, 1900, 5355, 137) and u.sport not in (53, 5353):
|
||||
key = (ipl.dst, u.dport) if u.dport < u.sport else (ipl.src, u.sport)
|
||||
udp_first.setdefault(key, float(p.time) - t0)
|
||||
|
||||
def label(ip):
|
||||
return snis.get(ip) or ip2host.get(ip, "")
|
||||
|
||||
print("=== DNS queries (in order) ===")
|
||||
for ts, q in dns_order:
|
||||
print(" +%7.2fs %s" % (ts, q))
|
||||
|
||||
print("\n=== TCP destinations (first SYN) ===")
|
||||
for (ip, port), ts in sorted(tcp_first.items(), key=lambda x: x[1]):
|
||||
print(" +%7.2fs %-17s :%-5s %s" % (ts, ip, port, label(ip)))
|
||||
|
||||
print("\n=== UDP destinations ===")
|
||||
for (ip, port), ts in sorted(udp_first.items(), key=lambda x: x[1])[:20]:
|
||||
print(" +%7.2fs %-17s :%-5s %s" % (ts, ip, port, label(ip)))
|
||||
|
||||
# ---- the three backends we care about ----
|
||||
print("\n=== BACKENDS DETECTED ===")
|
||||
allhosts = set(q for _, q in dns_order) | set(snis.values()) | set(ip2host.values())
|
||||
found = False
|
||||
for h in sorted(allhosts):
|
||||
m = PLAYFAB.match(h)
|
||||
if m:
|
||||
print(" PlayFab host=%s ** TitleId = %s **" % (h, m.group(1).upper()))
|
||||
found = True
|
||||
elif HOLOGRYPH.match(h) and "gameclient" not in h:
|
||||
print(" Master server host=%s (region=%s, ws://80 cleartext)"
|
||||
% (h, HOLOGRYPH.match(h).group(1)))
|
||||
found = True
|
||||
elif "sandconfigstorage" in h:
|
||||
print(" Config CDN host=%s (anonymous HTTPS)" % h)
|
||||
found = True
|
||||
if not found:
|
||||
print(" (none of PlayFab / hologryph / config-blob seen — backend not contacted)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
87
reverse/ghidra_apply_il2cpp_symbols.py
Normal file
87
reverse/ghidra_apply_il2cpp_symbols.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Headless-adapted Il2CppDumper -> Ghidra symbol applier ("fake PDB" from script.json).
|
||||
# Names every method, string literal and metadata entry, and creates function boundaries
|
||||
# from the dumper's address table -- WITHOUT Ghidra having to rediscover them via analysis.
|
||||
# Light path: names + function starts only (no il2cpp.h struct import, no signatures).
|
||||
#
|
||||
# Adapted from yoten/ghidra.py: replaced the interactive askFile() with a script arg / default
|
||||
# path so it runs under analyzeHeadless. Jython 2.7.
|
||||
#
|
||||
# Run (after the program is imported into the project):
|
||||
# analyzeHeadless ghidra/project SAND -process GameAssembly.dll -noanalysis \
|
||||
# -scriptPath ghidra/scripts -postScript apply_il2cpp_symbols.py [path/to/script.json]
|
||||
# Default script.json: the live yoten dump.
|
||||
# @category il2cpp
|
||||
import json
|
||||
|
||||
DEFAULT_SCRIPT_JSON = "/mnt/c/Users/downloadpizza/Downloads/yoten/script.json"
|
||||
|
||||
processFields = ["ScriptMethod", "ScriptString", "ScriptMetadata", "ScriptMetadataMethod", "Addresses"]
|
||||
|
||||
baseAddress = currentProgram.getImageBase()
|
||||
USER_DEFINED = ghidra.program.model.symbol.SourceType.USER_DEFINED
|
||||
|
||||
|
||||
def get_addr(addr):
|
||||
return baseAddress.add(addr)
|
||||
|
||||
|
||||
def set_name(addr, name):
|
||||
try:
|
||||
createLabel(addr, name.replace(' ', '-'), True, USER_DEFINED)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def make_function(start):
|
||||
if getFunctionAt(start) is None:
|
||||
try:
|
||||
createFunction(start, None)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
args = getScriptArgs()
|
||||
path = args[0] if args else DEFAULT_SCRIPT_JSON
|
||||
print("apply_il2cpp_symbols: loading " + path)
|
||||
data = json.loads(open(path, 'rb').read().decode('utf-8'))
|
||||
|
||||
if "Addresses" in data and "Addresses" in processFields:
|
||||
addresses = data["Addresses"]
|
||||
monitor.initialize(len(addresses))
|
||||
monitor.setMessage("Function boundaries")
|
||||
for index in range(len(addresses) - 1):
|
||||
make_function(get_addr(addresses[index]))
|
||||
monitor.incrementProgress(1)
|
||||
|
||||
if "ScriptMethod" in data and "ScriptMethod" in processFields:
|
||||
scriptMethods = data["ScriptMethod"]
|
||||
monitor.initialize(len(scriptMethods))
|
||||
monitor.setMessage("Methods")
|
||||
for sm in scriptMethods:
|
||||
set_name(get_addr(sm["Address"]), sm["Name"].encode("utf-8"))
|
||||
monitor.incrementProgress(1)
|
||||
|
||||
if "ScriptString" in data and "ScriptString" in processFields:
|
||||
scriptStrings = data["ScriptString"]
|
||||
monitor.initialize(len(scriptStrings))
|
||||
monitor.setMessage("Strings")
|
||||
for i, ss in enumerate(scriptStrings, 1):
|
||||
addr = get_addr(ss["Address"])
|
||||
createLabel(addr, "StringLiteral_" + str(i), True, USER_DEFINED)
|
||||
setEOLComment(addr, ss["Value"].encode("utf-8"))
|
||||
monitor.incrementProgress(1)
|
||||
|
||||
if "ScriptMetadata" in data and "ScriptMetadata" in processFields:
|
||||
for md in data["ScriptMetadata"]:
|
||||
addr = get_addr(md["Address"])
|
||||
set_name(addr, md["Name"].encode("utf-8"))
|
||||
setEOLComment(addr, md["Name"].encode("utf-8"))
|
||||
|
||||
if "ScriptMetadataMethod" in data and "ScriptMetadataMethod" in processFields:
|
||||
for mdm in data["ScriptMetadataMethod"]:
|
||||
addr = get_addr(mdm["Address"])
|
||||
set_name(addr, mdm["Name"].encode("utf-8"))
|
||||
setEOLComment(addr, mdm["Name"].encode("utf-8"))
|
||||
|
||||
print("apply_il2cpp_symbols: done")
|
||||
265
reverse/master_scrape.py
Normal file
265
reverse/master_scrape.py
Normal file
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scrape SAND's master server by replaying its protocol (2026-06-15 build) from an external
|
||||
process - no game, no BattlEye interaction (like playfab_scrape.py).
|
||||
|
||||
HANDSHAKE (RE'd from ghidra decompile of GameAssembly.dll + live Player.log; build 23737037):
|
||||
TWO sequential WebSocket connections, both speaking ClientMessage JSON envelopes:
|
||||
envelope = {"Id":<long>, "ClientMessageType":<int>, "Action":<int>, "Message":<string|null>}
|
||||
ClientMessageType: Error=0, Ping=1, Action=2, Event=3 (requests use 2)
|
||||
Action (ClientAction): Login=0, Connect=1, GetCharacters=2, GetStorage=20, GetNews=21,
|
||||
GetCompartmentDefinitions=23, GetDatabaseGuid=26, GetDefaultWalkerBlueprints=28,
|
||||
GetServers=29, GetResearchTree=32, GetShopItems=39, ...
|
||||
`Message` for Login is the LoginRequest serialized as a JSON *string* (nested); for Connect it
|
||||
is the bool isDebug stringified ("False"); for parameterless gets it is null.
|
||||
Serialization: Newtonsoft default -> PascalCase fields, enums as integers, sent as TEXT frames.
|
||||
Replies: a ClientMessage with the same Id; its `Message` is JSON of OperationResult<T> =
|
||||
{"IsSucceed":bool,"Error":string|null,"Status":int|null,"Result":<T>}. Correlate by Id.
|
||||
|
||||
Step 1 /login (no auth header): connect wss://<region>.hologryph.com/gameclient/login,
|
||||
send ClientMessage{Action=Login(0), Message=JSON(LoginRequest{SessionTicket=<PlayFab>,
|
||||
Title=56693, Platform=STEAM(1), ClientVersion=<git hash>})}; reply Result.SessionTicket
|
||||
is the SERVER session ticket. Close.
|
||||
Step 2 /connect (persistent): connect wss://<region>.hologryph.com/gameclient/connect WITH
|
||||
HTTP upgrade header Authorization: <server SessionTicket> ; send Connect(Action=1,
|
||||
Message="False"); then all data ops over this same socket.
|
||||
|
||||
SAFETY: does nothing over the network without --go. Token from reverse/.secrets/playfab_token.json.
|
||||
|
||||
Usage:
|
||||
venv/bin/python reverse/master_scrape.py --selftest # offline
|
||||
venv/bin/python reverse/master_scrape.py # dry run (plan only)
|
||||
venv/bin/python reverse/master_scrape.py --go --data # ARMED: full handshake + data ops
|
||||
"""
|
||||
import asyncio, json, os, argparse, ssl, sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SECRETS = os.path.join(ROOT, "reverse", ".secrets", "playfab_token.json")
|
||||
NAMES_PATH = os.path.join(ROOT, "extracted", "item_names.json")
|
||||
OUTDIR = os.path.join(ROOT, "extracted")
|
||||
|
||||
REGION_BASES = {r: "wss://%s.hologryph.com/gameclient/" % r
|
||||
for r in ("ger", "eus", "westus", "sin", "bra", "aus", "sko", "uae")}
|
||||
MT_ACTION = 2
|
||||
ACTIONS = {"Login": 0, "Connect": 1, "GetCharacters": 2, "GetExpedition": 7, "SelectCharacter": 4, "GetTrampler": 16,
|
||||
"UpsertTrampler": 17, "GetStorage": 20, "GetNews": 21, "GetCompartmentDefinitions": 23,
|
||||
"GetDatabaseGuid": 26, "GetDefaultWalkerBlueprints": 28, "GetServers": 29,
|
||||
"GetResearchTree": 32, "GetShopItems": 39}
|
||||
PLATFORM_STEAM = 1
|
||||
DATA_OPS = ["GetCompartmentDefinitions", "GetShopItems", "GetResearchTree", "GetServers",
|
||||
"GetDefaultWalkerBlueprints", "GetNews", "GetDatabaseGuid"]
|
||||
USER_OPS = ["GetStorage", "GetCharacters"]
|
||||
|
||||
|
||||
def load_names():
|
||||
try:
|
||||
d = json.load(open(NAMES_PATH))["items"]
|
||||
return {k: (v.get("name") or k) for k, v in d.items()}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def load_token():
|
||||
try:
|
||||
return json.load(open(SECRETS))
|
||||
except Exception as e:
|
||||
return {"_error": str(e)}
|
||||
|
||||
|
||||
def mask(s):
|
||||
return (s[:6] + "..." + s[-4:]) if s and len(s) > 12 else "<none>"
|
||||
|
||||
|
||||
def login_request(tok, version):
|
||||
return {"SessionTicket": tok["SessionTicket"], "Title": tok.get("TitleId", "56693"),
|
||||
"Platform": PLATFORM_STEAM, "ClientVersion": version}
|
||||
|
||||
|
||||
class WS:
|
||||
"""One ClientMessage WebSocket: connect (opt. Authorization header) -> id-correlated requests."""
|
||||
def __init__(self, url, auth=None, insecure=False):
|
||||
self.url = url
|
||||
self.auth = auth
|
||||
self.ssl = ssl.create_default_context()
|
||||
if insecure:
|
||||
self.ssl.check_hostname = False
|
||||
self.ssl.verify_mode = ssl.CERT_NONE
|
||||
self._id = 0
|
||||
self._pending = {}
|
||||
self.events = []
|
||||
self.ws = None
|
||||
|
||||
async def connect(self):
|
||||
import websockets
|
||||
headers = {"Authorization": self.auth} if self.auth else None
|
||||
self.ws = await websockets.connect(self.url, ssl=self.ssl, open_timeout=15,
|
||||
max_size=None, ping_interval=None,
|
||||
additional_headers=headers)
|
||||
self._task = asyncio.create_task(self._recv())
|
||||
|
||||
async def _recv(self):
|
||||
try:
|
||||
async for raw in self.ws:
|
||||
if isinstance(raw, bytes):
|
||||
raw = raw.decode("utf-8", "replace")
|
||||
try:
|
||||
msg = json.loads(raw)
|
||||
except Exception:
|
||||
self.events.append({"_unparsed": raw[:300]}); continue
|
||||
if msg.get("ClientMessageType") == 1: # Ping -> echo
|
||||
try:
|
||||
await self.ws.send(json.dumps(msg))
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
rid = msg.get("Id")
|
||||
if rid in self._pending and not self._pending[rid].done():
|
||||
self._pending[rid].set_result(msg)
|
||||
else:
|
||||
self.events.append(msg)
|
||||
except Exception as e:
|
||||
for f in self._pending.values():
|
||||
if not f.done():
|
||||
f.set_exception(e)
|
||||
|
||||
async def request(self, action_name, payload=None, raw_message=None, timeout=30):
|
||||
self._id += 1
|
||||
rid = self._id
|
||||
if raw_message is not None:
|
||||
message = raw_message
|
||||
elif payload is not None:
|
||||
message = json.dumps(payload)
|
||||
else:
|
||||
message = None
|
||||
fut = asyncio.get_event_loop().create_future()
|
||||
self._pending[rid] = fut
|
||||
await self.ws.send(json.dumps({"Id": rid, "ClientMessageType": MT_ACTION,
|
||||
"Action": ACTIONS[action_name], "Message": message}))
|
||||
reply = await asyncio.wait_for(fut, timeout=timeout)
|
||||
inner = reply.get("Message")
|
||||
if not isinstance(inner, str) or not inner:
|
||||
return inner
|
||||
try:
|
||||
return json.loads(inner)
|
||||
except ValueError:
|
||||
return inner # non-JSON Message (e.g. plain string / empty result)
|
||||
|
||||
async def close(self):
|
||||
if self.ws:
|
||||
await self.ws.close()
|
||||
|
||||
|
||||
def resolve(obj, names):
|
||||
if isinstance(obj, dict):
|
||||
out = {}
|
||||
for k, v in obj.items():
|
||||
out[k] = resolve(v, names)
|
||||
if k in ("DefinitionName", "ItemDefinition", "EpbId") and isinstance(v, str) and v in names:
|
||||
out["_name"] = names[v]
|
||||
return out
|
||||
if isinstance(obj, list):
|
||||
return [resolve(x, names) for x in obj]
|
||||
return obj
|
||||
|
||||
|
||||
def unwrap(msg):
|
||||
"""Replies are either the DTO directly, or an OperationResult{IsSucceed,Result}. Normalize."""
|
||||
if isinstance(msg, dict) and "IsSucceed" in msg and "Result" in msg:
|
||||
return msg["Result"]
|
||||
return msg
|
||||
|
||||
|
||||
def save(op_name, result, names):
|
||||
out = os.path.join(OUTDIR, "master_%s.json" % op_name)
|
||||
json.dump({"_op": op_name, "Result": resolve(result, names)},
|
||||
open(out, "w"), indent=1, ensure_ascii=False, default=str)
|
||||
print(" wrote %s (%s)" % (out, len(result) if isinstance(result, list) else 1))
|
||||
|
||||
|
||||
async def run(args):
|
||||
base = REGION_BASES[args.region]
|
||||
names = load_names()
|
||||
tok = load_token()
|
||||
version = args.client_version or tok.get("ClientVersion", "")
|
||||
|
||||
# --- Step 1: /login (no header) -> server SessionTicket ---
|
||||
print("STEP 1 /login %slogin" % base)
|
||||
lg = WS(base + "login", insecure=args.insecure)
|
||||
await lg.connect()
|
||||
res = await lg.request("Login", payload=login_request(tok, version)) # Message = LoginUserResultDto
|
||||
await lg.close()
|
||||
res = unwrap(res)
|
||||
srv_ticket = res.get("SessionTicket") if isinstance(res, dict) else None
|
||||
if not srv_ticket:
|
||||
print(" login failed; reply:", json.dumps(res)[:300]); return
|
||||
user = res.get("User", {})
|
||||
print(" Login OK. User.Id=%s name=%s -> server ticket %s" %
|
||||
(user.get("Id"), user.get("DisplayName"), mask(srv_ticket)))
|
||||
save("Login", res, names)
|
||||
|
||||
# --- Step 2: /connect (Authorization header) -> persistent socket ---
|
||||
print("STEP 2 /connect %sconnect (Authorization: <server ticket>)" % base)
|
||||
cx = WS(base + "connect", auth=srv_ticket, insecure=args.insecure)
|
||||
await cx.connect()
|
||||
cop = await cx.request("Connect", raw_message="False")
|
||||
print(" Connect ->", json.dumps(cop)[:160] if cop is not None else "(no body)")
|
||||
|
||||
ops = (DATA_OPS if args.data or not args.user else []) + (USER_OPS if args.user else [])
|
||||
for name in ops:
|
||||
try:
|
||||
res = unwrap(await cx.request(name))
|
||||
n = len(res) if isinstance(res, list) else (1 if res else 0)
|
||||
print(" %-28s -> %s entr%s" % (name, n, "y" if n == 1 else "ies"))
|
||||
if res is not None:
|
||||
save(name, res, names)
|
||||
except Exception as e:
|
||||
print(" %-28s ERROR %s: %s" % (name, type(e).__name__, e))
|
||||
await cx.close()
|
||||
print("done. events: %d" % len(cx.events))
|
||||
|
||||
|
||||
def print_plan(args):
|
||||
base = REGION_BASES[args.region]
|
||||
tok = load_token()
|
||||
ver = args.client_version or tok.get("ClientVersion", "<VERSION>")
|
||||
print("=== DRY RUN (no network). Pass --go to connect. ===")
|
||||
print("STEP 1 connect %slogin (no auth header)" % base)
|
||||
lr = login_request({"SessionTicket": "<PLAYFAB_TICKET>", "TitleId": tok.get("TitleId", "56693")}, ver)
|
||||
print(" send {\"Id\":1,\"ClientMessageType\":2,\"Action\":0,\"Message\":%s}" % json.dumps(json.dumps(lr)))
|
||||
print(" (real PlayFab ticket %s) -> reply Result.SessionTicket = server ticket" %
|
||||
mask(tok.get("SessionTicket", "")))
|
||||
print("STEP 2 connect %sconnect HEADER Authorization: <server ticket>" % base)
|
||||
print(" send {\"Id\":1,\"ClientMessageType\":2,\"Action\":1,\"Message\":\"False\"}")
|
||||
for i, name in enumerate(DATA_OPS, 2):
|
||||
print(" then {\"Id\":%d,\"ClientMessageType\":2,\"Action\":%d,\"Message\":null} (%s)"
|
||||
% (i, ACTIONS[name], name))
|
||||
print("\nNothing sent.")
|
||||
|
||||
|
||||
def selftest():
|
||||
lr = login_request({"SessionTicket": "T", "TitleId": "56693"}, "abc")
|
||||
assert lr == {"SessionTicket": "T", "Title": "56693", "Platform": 1, "ClientVersion": "abc"}, lr
|
||||
assert ACTIONS["Login"] == 0 and ACTIONS["Connect"] == 1 and ACTIONS["GetCompartmentDefinitions"] == 23
|
||||
names = {"item_coinCrown": "Crowns"}
|
||||
assert resolve({"ItemDefinition": "item_coinCrown"}, names)["_name"] == "Crowns"
|
||||
print("selftest OK: login payload, actions, name resolution")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="SAND master-server replay (two-socket ClientMessage)")
|
||||
ap.add_argument("--region", default="ger", choices=list(REGION_BASES))
|
||||
ap.add_argument("--go", action="store_true")
|
||||
ap.add_argument("--data", action="store_true")
|
||||
ap.add_argument("--user", action="store_true")
|
||||
ap.add_argument("--client-version", default="")
|
||||
ap.add_argument("--insecure", action="store_true")
|
||||
ap.add_argument("--selftest", action="store_true")
|
||||
args = ap.parse_args()
|
||||
if args.selftest:
|
||||
selftest(); return
|
||||
if not args.go:
|
||||
print_plan(args); return
|
||||
asyncio.run(run(args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
108
reverse/noise_filter.py
Normal file
108
reverse/noise_filter.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Baseline-subtraction noise filter for SAND captures.
|
||||
|
||||
Idea: capture a short "noise baseline" with SAND NOT running, then capture the real
|
||||
session with SAND running. Every IP/host in the baseline is pre-SAND noise; subtract it
|
||||
and what's left is (almost entirely) the game's traffic.
|
||||
|
||||
Usage:
|
||||
# 1) just list the noise in a baseline:
|
||||
venv/bin/python reverse/noise_filter.py baseline.pcapng
|
||||
|
||||
# 2) baseline + session -> hosts unique to the session + a ready Wireshark filter:
|
||||
venv/bin/python reverse/noise_filter.py baseline.pcapng session.pcapng
|
||||
|
||||
Outputs, for the session run:
|
||||
- the new (non-noise) IPs/hosts, sorted by traffic volume
|
||||
- a Wireshark *display* filter: ip.addr==X or ip.addr==Y ...
|
||||
- a *capture* (BPF) filter to EXCLUDE noise next time: not (host X or host Y ...)
|
||||
"""
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from scapy.all import rdpcap, DNS, DNSQR, IP, IPv6, TCP, UDP, Raw
|
||||
|
||||
sys.path.insert(0, __file__.rsplit("/", 1)[0])
|
||||
from capture_hosts import tls_sni # reuse the SNI parser
|
||||
|
||||
# hosts/IPs that are never the game, even if they appear only in the session
|
||||
ALWAYS_NOISE_SUBSTR = ("anthropic.com", "datadoghq.com", "windowsupdate", "msftncsi",
|
||||
"msftconnecttest", "ntp.", ".pool.ntp.org")
|
||||
|
||||
|
||||
def scan(path):
|
||||
"""Return (ip_volume, ip2host) for a pcap.
|
||||
ip_volume[ip] = packet count to/from that remote ip; ip2host[ip] = best label."""
|
||||
pk = rdpcap(path)
|
||||
vol = defaultdict(int)
|
||||
ip2host, dns = {}, {}
|
||||
# learn DNS answers (qname for an ip) and SNI
|
||||
for p in pk:
|
||||
if p.haslayer(DNS) and p[DNS].qr == 1 and p[DNS].ancount:
|
||||
try:
|
||||
qn = p[DNSQR].qname.decode(errors="replace").rstrip(".")
|
||||
for k in range(p[DNS].ancount):
|
||||
rr = p[DNS].an[k]
|
||||
if rr.type in (1, 28):
|
||||
dns[str(rr.rdata)] = qn
|
||||
except Exception:
|
||||
pass
|
||||
if p.haslayer(TCP) and p.haslayer(Raw):
|
||||
s = tls_sni(bytes(p[Raw].load))
|
||||
if s and (p.haslayer(IP) or p.haslayer(IPv6)):
|
||||
ipl = p[IP] if p.haslayer(IP) else p[IPv6]
|
||||
ip2host[ipl.dst] = s
|
||||
ipl = p[IP] if p.haslayer(IP) else (p[IPv6] if p.haslayer(IPv6) else None)
|
||||
if ipl is None:
|
||||
continue
|
||||
for ip in (ipl.src, ipl.dst):
|
||||
if not is_local(ip):
|
||||
vol[ip] += 1
|
||||
for ip in vol:
|
||||
ip2host.setdefault(ip, dns.get(ip, ""))
|
||||
return vol, ip2host
|
||||
|
||||
|
||||
def is_local(ip):
|
||||
return (ip.startswith(("10.", "192.168.", "127.", "169.254.", "fe80:", "ff", "::1"))
|
||||
or ip.startswith("172.") and 16 <= int(ip.split(".")[1] or 0) <= 31
|
||||
or ip in ("0.0.0.0",) or ip.endswith(".255"))
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit(__doc__)
|
||||
base_vol, base_host = scan(sys.argv[1])
|
||||
noise = set(base_vol)
|
||||
print("=== baseline noise: %d remote IPs ===" % len(noise))
|
||||
for ip, n in sorted(base_vol.items(), key=lambda x: -x[1]):
|
||||
print(" %-16s %-6d %s" % (ip, n, base_host.get(ip, "")))
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print("\n(pass a second pcap to diff a real session against this baseline)")
|
||||
return
|
||||
|
||||
sess_vol, sess_host = scan(sys.argv[2])
|
||||
# a session ip is "game" if not in baseline and not on the always-noise list
|
||||
def always_noise(ip):
|
||||
h = sess_host.get(ip, "")
|
||||
return any(s in h for s in ALWAYS_NOISE_SUBSTR)
|
||||
|
||||
new = {ip: n for ip, n in sess_vol.items()
|
||||
if ip not in noise and not always_noise(ip)}
|
||||
print("\n=== session-only hosts (candidate SAND backends) ===")
|
||||
for ip, n in sorted(new.items(), key=lambda x: -x[1]):
|
||||
print(" %-16s %-6d %s" % (ip, n, sess_host.get(ip, "")))
|
||||
if not new:
|
||||
print(" (nothing new — either SAND made no new connections, or it reused a "
|
||||
"baseline IP/CDN; widen the gap or capture longer)")
|
||||
return
|
||||
|
||||
ips = sorted(new)
|
||||
print("\n--- Wireshark DISPLAY filter (keep only SAND) ---")
|
||||
print(" " + " or ".join("ip.addr==%s" % ip for ip in ips))
|
||||
print("\n--- Wireshark CAPTURE filter (BPF, EXCLUDE noise next time) ---")
|
||||
print(" not (" + " or ".join("host %s" % ip for ip in sorted(noise)) + ")")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
184
reverse/playfab_scrape.py
Normal file
184
reverse/playfab_scrape.py
Normal file
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scrape SAND's PlayFab Economy (catalog + prices + inventory) via the public REST API.
|
||||
|
||||
Backend is Azure PlayFab (Economy v2). Auth is `LoginWithSteam`. Everything here is the
|
||||
documented PlayFab REST surface (https://learn.microsoft.com/gaming/playfab/), run from
|
||||
*outside* the game process — no BattlEye interaction.
|
||||
|
||||
Two ways in:
|
||||
--steam-ticket <hex> do Client/LoginWithSteam to get a fresh SessionTicket + EntityToken
|
||||
--entity-token <tok> skip login, use an EntityToken you captured (e.g. from your MITM)
|
||||
|
||||
The only constant you must supply is --title-id (the PlayFab TitleId). Get it from a live
|
||||
capture: `reverse/capture_hosts.py` prints it from the `<titleId>.playfabapi.com` host.
|
||||
|
||||
Modes (combine freely):
|
||||
--catalog Catalog/SearchItems -> every item with PriceOptions + DisplayProperties
|
||||
--inventory Inventory/GetInventoryItems + GetTransactionHistory (your wallet/items)
|
||||
--titledata Client/GetTitleData (config key/values, if any)
|
||||
|
||||
Read-only endpoints only. Output -> extracted/playfab_<mode>.json, item ids resolved to names.
|
||||
|
||||
Example:
|
||||
venv/bin/python reverse/playfab_scrape.py --title-id ABCDE --steam-ticket 14000000... --catalog
|
||||
"""
|
||||
import sys, os, json, argparse
|
||||
import requests
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
NAMES_PATH = os.path.join(ROOT, "extracted", "item_names.json")
|
||||
|
||||
|
||||
def load_names():
|
||||
try:
|
||||
d = json.load(open(NAMES_PATH))["items"]
|
||||
return {k: (v.get("name") or k) for k, v in d.items()}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
class PlayFab:
|
||||
def __init__(self, title_id):
|
||||
self.title = title_id
|
||||
self.base = "https://%s.playfabapi.com" % title_id
|
||||
self.session_ticket = None
|
||||
self.entity_token = None
|
||||
self.s = requests.Session()
|
||||
self.s.headers["Content-Type"] = "application/json"
|
||||
|
||||
def _post(self, path, body, headers=None):
|
||||
r = self.s.post(self.base + path, data=json.dumps(body),
|
||||
headers=headers or {}, timeout=30)
|
||||
try:
|
||||
j = r.json()
|
||||
except Exception:
|
||||
r.raise_for_status()
|
||||
raise
|
||||
if r.status_code != 200 or j.get("code") not in (200, None):
|
||||
raise RuntimeError("%s -> %s %s" % (path, r.status_code,
|
||||
j.get("errorMessage") or j.get("error") or j))
|
||||
return j.get("data", j)
|
||||
|
||||
def login_steam(self, ticket_hex, service_specific=False):
|
||||
body = {"TitleId": self.title, "SteamTicket": ticket_hex,
|
||||
"CreateAccount": False, "TicketIsServiceSpecific": service_specific}
|
||||
d = self._post("/Client/LoginWithSteam", body)
|
||||
self.session_ticket = d.get("SessionTicket")
|
||||
et = d.get("EntityToken") or {}
|
||||
self.entity_token = et.get("EntityToken")
|
||||
print(" logged in: PlayFabId=%s entity=%s" %
|
||||
(d.get("PlayFabId"), (et.get("Entity") or {}).get("Id")))
|
||||
return d
|
||||
|
||||
# ---- Economy v2 (Entity API; needs X-EntityToken) ----
|
||||
def search_items(self):
|
||||
items, token = [], None
|
||||
while True:
|
||||
body = {"Count": 50, "Filter": "", "Search": ""}
|
||||
if token:
|
||||
body["ContinuationToken"] = token
|
||||
d = self._post("/Catalog/SearchItems", body,
|
||||
headers={"X-EntityToken": self.entity_token})
|
||||
items += d.get("Items", [])
|
||||
token = d.get("ContinuationToken")
|
||||
print(" catalog: %d items so far..." % len(items))
|
||||
if not token:
|
||||
break
|
||||
return items
|
||||
|
||||
def inventory(self):
|
||||
out = {}
|
||||
out["items"] = self._post("/Inventory/GetInventoryItems", {"Count": 50},
|
||||
headers={"X-EntityToken": self.entity_token}).get("Items", [])
|
||||
try:
|
||||
out["transactions"] = self._post("/Inventory/GetTransactionHistory", {"Count": 50},
|
||||
headers={"X-EntityToken": self.entity_token}).get("Transactions", [])
|
||||
except Exception as e:
|
||||
out["transactions_error"] = str(e)
|
||||
return out
|
||||
|
||||
def title_data(self):
|
||||
return self._post("/Client/GetTitleData", {},
|
||||
headers={"X-Authorization": self.session_ticket}).get("Data", {})
|
||||
|
||||
|
||||
def summarize_catalog(items, names):
|
||||
"""Flatten each catalog item to {id, friendlyId, name, type, prices, displayProperties}."""
|
||||
rows = []
|
||||
for it in items:
|
||||
alt = {a.get("Type"): a.get("Value") for a in it.get("AlternateIds", [])}
|
||||
fid = alt.get("FriendlyId") or it.get("Id")
|
||||
prices = []
|
||||
for po in (it.get("PriceOptions") or {}).get("Prices", []):
|
||||
legs = [{"currency": a.get("ItemId"),
|
||||
"currencyName": names.get(a.get("ItemId"), a.get("ItemId")),
|
||||
"amount": a.get("Amount")} for a in po.get("Amounts", [])]
|
||||
prices.append(legs)
|
||||
title = it.get("Title", {})
|
||||
rows.append({
|
||||
"id": it.get("Id"),
|
||||
"friendlyId": fid,
|
||||
"name": (title.get("NEUTRAL") or names.get(fid) or fid),
|
||||
"type": it.get("Type"),
|
||||
"contentType": it.get("ContentType"),
|
||||
"prices": prices,
|
||||
"displayProperties": it.get("DisplayProperties"),
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--title-id", required=True)
|
||||
ap.add_argument("--steam-ticket")
|
||||
ap.add_argument("--entity-token")
|
||||
ap.add_argument("--ticket-service-specific", action="store_true")
|
||||
ap.add_argument("--catalog", action="store_true")
|
||||
ap.add_argument("--inventory", action="store_true")
|
||||
ap.add_argument("--titledata", action="store_true")
|
||||
args = ap.parse_args()
|
||||
if not (args.steam_ticket or args.entity_token):
|
||||
sys.exit("need --steam-ticket OR --entity-token")
|
||||
if not (args.catalog or args.inventory or args.titledata):
|
||||
args.catalog = True # default
|
||||
|
||||
names = load_names()
|
||||
pf = PlayFab(args.title_id)
|
||||
if args.entity_token:
|
||||
pf.entity_token = args.entity_token
|
||||
print(" using captured EntityToken")
|
||||
if args.steam_ticket:
|
||||
pf.login_steam(args.steam_ticket, args.ticket_service_specific)
|
||||
|
||||
os.makedirs(os.path.join(ROOT, "extracted"), exist_ok=True)
|
||||
|
||||
if args.catalog:
|
||||
items = pf.search_items()
|
||||
rows = summarize_catalog(items, names)
|
||||
out = os.path.join(ROOT, "extracted", "playfab_catalog.json")
|
||||
json.dump({"_source": "PlayFab Catalog/SearchItems", "count": len(rows),
|
||||
"items_raw": items, "items": rows}, open(out, "w"),
|
||||
indent=1, ensure_ascii=False)
|
||||
print("wrote %s (%d items)" % (out, len(rows)))
|
||||
priced = [r for r in rows if r["prices"]]
|
||||
print(" %d items carry a price. sample:" % len(priced))
|
||||
for r in priced[:10]:
|
||||
pr = " | ".join(" + ".join("%s %s" % (l["amount"], l["currencyName"]) for l in legs)
|
||||
for legs in r["prices"])
|
||||
print(" %-32s %s" % (r["name"], pr))
|
||||
|
||||
if args.inventory and pf.entity_token:
|
||||
inv = pf.inventory()
|
||||
out = os.path.join(ROOT, "extracted", "playfab_inventory.json")
|
||||
json.dump(inv, open(out, "w"), indent=1, ensure_ascii=False)
|
||||
print("wrote", out)
|
||||
|
||||
if args.titledata and pf.session_ticket:
|
||||
td = pf.title_data()
|
||||
out = os.path.join(ROOT, "extracted", "playfab_titledata.json")
|
||||
json.dump(td, open(out, "w"), indent=1, ensure_ascii=False)
|
||||
print("wrote %s (%d keys)" % (out, len(td)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
95
reverse/render_trampler.py
Normal file
95
reverse/render_trampler.py
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
# Strictly data-derived: footprints (CompartmentsDatabase cells + blueprint rotation/origin),
|
||||
# doors/hatches (blueprint Connections), single-tile guns. NO firing arcs: the facing/aim is
|
||||
# NOT present in the data (no forward/aim/angle field), so it is intentionally omitted.
|
||||
import json, collections, os, re
|
||||
import math
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
bp=json.load(open(ROOT+"/extracted/host_trampler_blueprint.json"))
|
||||
db={c["entityId"]:c for c in json.load(open(ROOT+"/extracted/CompartmentsDatabase.json"))["compartments"]}
|
||||
def rot(x,z,deg):
|
||||
deg%=360; return {0:(x,z),90:(z,-x),180:(-x,-z),270:(-z,x)}[deg]
|
||||
def cat(e):
|
||||
m=re.match(r"walker_comp([A-Za-z]+)_",e or ""); return m.group(1) if m else "?"
|
||||
CC={"Chassis":(90,90,100),"Reactor":(255,140,0),"Engine":(210,60,45),"Weapon":(170,40,60),
|
||||
"Armor":(70,110,165),"Cargo":(210,170,60),"Crew":(70,165,85),"CaptainCrew":(30,110,55),
|
||||
"Deck":(205,200,185),"Corridor":(160,205,228),"Special":(150,80,190),"Crafting":(150,100,60),
|
||||
"Steering":(40,165,165),"Structure":(150,150,160),"Balcony":(190,170,140)}
|
||||
LET={k:k[0] for k in CC}; LET.update({"CaptainCrew":"K","Corridor":"+","Crew":"c","Crafting":"F","Steering":"T","Chassis":"#","Weapon":"W"})
|
||||
parts=[bp["Chassis"]]+bp["Compartments"]
|
||||
def is_turret(e): return e.startswith("walker_compWeapon_TurretSlot")
|
||||
def is_ram(e): return "BattleRam" in e
|
||||
floors=collections.defaultdict(dict); labels=collections.defaultdict(list); turrets=[]; cargo_n=0
|
||||
for p in parts:
|
||||
e=db.get(p["EpbId"]);
|
||||
if not e: continue
|
||||
eid=p["EpbId"]; ox,oy,oz=p["CellCoordinate"]["x"],p["CellCoordinate"]["y"],p["CellCoordinate"]["z"]; deg=int(p.get("Rotation",0)); c=cat(eid)
|
||||
if is_turret(eid):
|
||||
floors[oy][(ox,oz)]=(c,True); labels[oy].append((ox,oz,"W"))
|
||||
base=135 if "Corner" in eid else 90 # rot0: corner=SW, straight=S (your calibration)
|
||||
turrets.append((oy,ox,oz,(base-deg)%360)); continue
|
||||
cells=[]
|
||||
for cl in e["cells"]:
|
||||
rx,rz=rot(cl["position"]["x"],cl["position"]["z"],deg); wy=oy+cl["position"]["y"]
|
||||
floors[wy].setdefault((ox+rx,oz+rz),(c,bool(cl.get("volumeOccupied"))))
|
||||
if cl.get("volumeOccupied") and wy==oy: cells.append((ox+rx,oz+rz))
|
||||
if is_ram(eid):
|
||||
for (x,z) in cells: floors[oy][(x,z)]=(c,True)
|
||||
cx=sum(x for x,z in cells)/max(1,len(cells)); cz=sum(z for x,z in cells)/max(1,len(cells)); labels[oy].append((cx,cz,"R"))
|
||||
elif c=="Cargo":
|
||||
cargo_n+=1; labels[oy].append((ox,oz,"C%d"%cargo_n))
|
||||
else:
|
||||
labels[oy].append((ox,oz,LET.get(c,"?")))
|
||||
doors=collections.defaultdict(list); hatches=collections.defaultdict(list)
|
||||
for cn in bp["Connections"]:
|
||||
g=cn["GridCoordinate"]; d=cn["ActualDirection"]; sl=cn["SlotType"]; st=cn["State"]
|
||||
if sl=="DOOR" and st in ("DOOR","OPEN") and d["y"]==0: doors[g["y"]].append((g["x"],g["z"],d["x"],d["z"],st))
|
||||
if sl=="HATCH" and st=="DOOR": hatches[g["y"]].append((g["x"],g["z"],d["y"]))
|
||||
allc=[(x,z) for y in floors if -1<=y<=3 for (x,z) in floors[y]]
|
||||
xmin=min(x for x,z in allc); xmax=max(x for x,z in allc); zmin=min(z for x,z in allc); zmax=max(z for x,z in allc)
|
||||
W=xmax-xmin+1; H=zmax-zmin+1; CELL=46; PAD=14; GAP=22; TITLEH=24
|
||||
order=[y for y in sorted(floors,reverse=True) if -1<=y<=3]
|
||||
FN={3:"deck 3",2:"deck 2",1:"deck 1",0:"deck 0",-1:"base / hull"}
|
||||
panelW=W*CELL; panelH=H*CELL; cols=len(order)
|
||||
imgW=PAD+cols*(panelW+GAP); imgH=PAD+TITLEH*2+panelH+120
|
||||
img=Image.new("RGBA",(imgW,imgH),(245,245,248,255)); ov=Image.new("RGBA",img.size,(0,0,0,0)); d=ImageDraw.Draw(img); od=ImageDraw.Draw(ov)
|
||||
f=ImageFont.load_default(size=18); fs=ImageFont.load_default(size=12); ft=ImageFont.load_default(size=15)
|
||||
d.text((PAD,4),"Host trampler - data-derived: footprints, doors/hatches, single-tile guns (W). Gun arc=rotation field (corner=45/straight=cardinal, +-90). N=up",fill=(20,20,30),font=ft)
|
||||
for i,y in enumerate(order):
|
||||
ox0=PAD+i*(panelW+GAP); oy0=PAD+TITLEH*2
|
||||
d.text((ox0,oy0-18),FN.get(y,"y%d"%y),fill=(20,20,30),font=ft)
|
||||
def px(x,z): return ox0+(x-xmin)*CELL, oy0+(z-zmin)*CELL
|
||||
for zi in range(H):
|
||||
for xi in range(W):
|
||||
X0=ox0+xi*CELL; Y0=oy0+zi*CELL; d.rectangle([X0,Y0,X0+CELL,Y0+CELL],outline=(225,225,228))
|
||||
for (x,z),(c,solid) in floors[y].items():
|
||||
X0,Y0=px(x,z); col=CC.get(c,(120,120,120))
|
||||
if solid: d.rectangle([X0+1,Y0+1,X0+CELL-1,Y0+CELL-1],fill=col,outline=(40,40,40))
|
||||
else: d.rectangle([X0+4,Y0+4,X0+CELL-4,Y0+CELL-4],fill=tuple(min(255,v+70) for v in col),outline=col)
|
||||
for (cx,cz,let) in labels[y]:
|
||||
X0,Y0=px(cx,cz); d.text((X0+CELL/2-5*len(let),Y0+CELL/2-9),let,fill=(255,255,255),font=f)
|
||||
for (x,z,dx,dz,st) in doors.get(y,[]):
|
||||
X0,Y0=px(x,z); col=(60,200,90) if st=="OPEN" else (130,80,30)
|
||||
if dx==1: d.line([X0+CELL,Y0+8,X0+CELL,Y0+CELL-8],fill=col,width=5)
|
||||
if dx==-1: d.line([X0,Y0+8,X0,Y0+CELL-8],fill=col,width=5)
|
||||
if dz==1: d.line([X0+8,Y0+CELL,X0+CELL-8,Y0+CELL],fill=col,width=5)
|
||||
if dz==-1: d.line([X0+8,Y0,X0+CELL-8,Y0],fill=col,width=5)
|
||||
for (x,z,dy) in hatches.get(y,[]):
|
||||
X0,Y0=px(x,z); cx,cy=X0+CELL/2,Y0+CELL/2; d.ellipse([cx-8,cy-8,cx+8,cy+8],outline=(20,20,140),width=2)
|
||||
d.text((cx-3,cy-7),"^" if dy==1 else "v",fill=(20,20,140),font=fs)
|
||||
for (ty,tx,tz,face) in turrets:
|
||||
if ty!=y: continue
|
||||
X0,Y0=px(tx,tz); cx,cy=X0+CELL/2,Y0+CELL/2; R=CELL*0.75
|
||||
od.pieslice([cx-R,cy-R,cx+R,cy+R],face-90,face+90,fill=(255,210,40,75),outline=(230,160,0,200))
|
||||
img=Image.alpha_composite(img,ov); d=ImageDraw.Draw(img)
|
||||
ly=PAD+TITLEH*2+panelH+10; d.text((PAD,ly-2),"LEGEND:",fill=(20,20,30),font=ft); ly+=18; lx=PAD
|
||||
for k in CC:
|
||||
d.rectangle([lx,ly,lx+20,ly+20],fill=CC[k],outline=(30,30,30)); d.text((lx+4,ly+4),LET.get(k,"?"),fill=(255,255,255),font=fs)
|
||||
d.text((lx+26,ly+4),k,fill=(20,20,30),font=fs); lx+=26+len(k)*7+18
|
||||
if lx>imgW-150: lx=PAD; ly+=26
|
||||
ly+=8
|
||||
d.line([PAD,ly,PAD+40,ly],fill=(130,80,30),width=5); d.text((PAD+48,ly-7),"door",fill=(20,20,30),font=fs)
|
||||
d.line([PAD+100,ly,PAD+140,ly],fill=(60,200,90),width=5); d.text((PAD+148,ly-7),"open door",fill=(20,20,30),font=fs)
|
||||
d.ellipse([PAD+230,ly-8,PAD+246,ly+8],outline=(20,20,140),width=2); d.text((PAD+254,ly-7),"hatch (^/v) W=gun R=ram yellow wedge=firing arc (+-90) faded=non-solid",fill=(20,20,30),font=fs)
|
||||
img.convert("RGB").save(ROOT+"/extracted/host_trampler_nice.png"); print("saved with rotation arcs, turrets=%d"%len(turrets))
|
||||
66
reverse/resolve_decomp.py
Normal file
66
reverse/resolve_decomp.py
Normal file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Annotate ghidra/decomp.c: resolve absolute VAs (0x180........) to IL2CPP symbol names
|
||||
(methods.tsv) and string literals (strings.tsv). Ghidra image base = 0x180000000.
|
||||
|
||||
methods.tsv : <rva-decimal>\t<symbol> (rva = scriptAddress - 0x1000)
|
||||
strings.tsv : <scriptAddress-decimal>\t<str> (raw ScriptString.Address)
|
||||
|
||||
Usage:
|
||||
venv/bin/python reverse/resolve_decomp.py # writes ghidra/decomp.annotated.c
|
||||
venv/bin/python reverse/resolve_decomp.py <substr> # print only funcs whose name matches substr
|
||||
"""
|
||||
import re, sys, os
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
G = os.path.join(ROOT, "ghidra")
|
||||
BASE = 0x180000000
|
||||
|
||||
def load_methods():
|
||||
m = {}
|
||||
for l in open(os.path.join(G, "methods.tsv")):
|
||||
if "\t" in l:
|
||||
rva, name = l.rstrip("\n").split("\t", 1)
|
||||
m[int(rva)] = name
|
||||
return m
|
||||
|
||||
def load_strings():
|
||||
s = {}
|
||||
p = os.path.join(G, "strings.tsv")
|
||||
if os.path.exists(p):
|
||||
for l in open(p):
|
||||
if "\t" in l:
|
||||
a, v = l.rstrip("\n").split("\t", 1)
|
||||
s[int(a)] = v
|
||||
return s
|
||||
|
||||
def main():
|
||||
methods = load_methods()
|
||||
strings = load_strings()
|
||||
src = open(os.path.join(G, "decomp.c"), encoding="utf-8", errors="replace").read()
|
||||
hexre = re.compile(r"0x(1[0-9a-fA-F]{8,9})")
|
||||
|
||||
def repl(m):
|
||||
va = int(m.group(1), 16)
|
||||
rva = va - BASE
|
||||
if rva in methods:
|
||||
return "%s/*%s*/" % (m.group(0), methods[rva])
|
||||
# string literal: try raw rva, rva+0x1000 (scriptAddress), and the table conventions
|
||||
for cand in (rva, rva + 0x1000, va, va - BASE + 0x1000):
|
||||
if cand in strings:
|
||||
return '%s/*"%s"*/' % (m.group(0), strings[cand][:60])
|
||||
return m.group(0)
|
||||
|
||||
out = hexre.sub(repl, src)
|
||||
outp = os.path.join(G, "decomp.annotated.c")
|
||||
open(outp, "w", encoding="utf-8").write(out)
|
||||
nres = out.count("/*")
|
||||
print("wrote %s (%d annotations)" % (outp, nres))
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
sub = sys.argv[1]
|
||||
blocks = re.split(r"(?=// ==== )", out)
|
||||
for b in blocks:
|
||||
if sub.lower() in b[:200].lower():
|
||||
print(b)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
76
reverse/trampler_hashes.py
Normal file
76
reverse/trampler_hashes.py
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the SAND trampler/walker blueprint hashes FROM SCRATCH (no harvesting needed).
|
||||
|
||||
RE'd from GameAssembly.dll (2026-06-15 build):
|
||||
MD5Utility.ComputeHash(obj) = MD5( UTF8( JsonConvert.SerializeObject(obj) ) ), formatted as
|
||||
uppercase hex with no separators (each byte -> ToString("X2")). [ghidra: MD5Utility$$ObjectToByteArray,
|
||||
$$ComputeHash]
|
||||
|
||||
The three top-level WalkerBlueprintDto hashes are MD5 of the *compact* JSON (Newtonsoft default:
|
||||
no whitespace, PascalCase keys in C# declaration order, nulls included, enums as STRING names,
|
||||
CellCoordinate as {x,y,z}) of:
|
||||
CompartmentsHash = ComputeHash(blueprint.Compartments) [List<CompartmentBlueprintDto>] VERIFIED
|
||||
ConnectionsHash = ComputeHash(blueprint.Connections) [List<ConnectionBlueprintDto>] VERIFIED <- "hash #3"
|
||||
DefinitionsHash = ComputeHash(<the CompartmentDefinitionDto list for the compartments>) PROVISIONAL
|
||||
(hashes the actual definitions, not blueprint-internal fields; verify live
|
||||
against a fresh GetTrampler + GetCompartmentDefinitions once unfrozen.)
|
||||
|
||||
To match Newtonsoft byte-for-byte, build the dicts with keys already in declaration order and
|
||||
serialize with json.dumps(obj, separators=(",",":"), ensure_ascii=False). Do NOT sort keys.
|
||||
"""
|
||||
import json, hashlib
|
||||
|
||||
|
||||
def md5_hash(obj) -> str:
|
||||
"""MD5Utility.ComputeHash(obj): MD5 of compact-JSON(obj), uppercase hex."""
|
||||
blob = json.dumps(obj, separators=(",", ":"), ensure_ascii=False)
|
||||
return hashlib.md5(blob.encode("utf-8")).hexdigest().upper()
|
||||
|
||||
|
||||
def compartments_hash(compartments: list) -> str:
|
||||
"""#1 CompartmentsHash = MD5(JSON(Compartments list)). VERIFIED."""
|
||||
return md5_hash(compartments)
|
||||
|
||||
|
||||
def connections_hash(connections: list) -> str:
|
||||
"""#3 ConnectionsHash = MD5(JSON(Connections list)). VERIFIED.
|
||||
|
||||
Each connection must serialize as (declaration order):
|
||||
{"Id": int, "EpbId": str|None, "ActualDirection": {"x":,"y":,"z":},
|
||||
"GridCoordinate": {"x":,"y":,"z":}, "SlotType": "<name>", "State": "<name>"}
|
||||
"""
|
||||
return md5_hash(connections)
|
||||
|
||||
|
||||
def definitions_hash(definitions: list) -> str:
|
||||
"""#2 DefinitionsHash = MD5(JSON(<CompartmentDefinitionDto list>)). PROVISIONAL — verify live."""
|
||||
return md5_hash(definitions)
|
||||
|
||||
|
||||
def apply_hashes(blueprint: dict, definitions: list = None) -> dict:
|
||||
"""Set the three top-level hashes on a blueprint dict in place. Returns it.
|
||||
definitions: ordered CompartmentDefinitionDto list for #2 (optional until verified)."""
|
||||
blueprint["CompartmentsHash"] = compartments_hash(blueprint["Compartments"])
|
||||
blueprint["ConnectionsHash"] = connections_hash(blueprint["Connections"])
|
||||
if definitions is not None:
|
||||
blueprint["DefinitionsHash"] = definitions_hash(definitions)
|
||||
return blueprint
|
||||
|
||||
|
||||
def _selftest():
|
||||
import os
|
||||
p = os.path.join(os.path.dirname(__file__), "..", "extracted",
|
||||
"playfab_titledata_TramplerBlueprint3.json")
|
||||
b = json.load(open(p))
|
||||
c1 = compartments_hash(b["Compartments"])
|
||||
c3 = connections_hash(b["Connections"])
|
||||
assert c1 == b["CompartmentsHash"], "CompartmentsHash %s != %s" % (c1, b["CompartmentsHash"])
|
||||
assert c3 == b["ConnectionsHash"], "ConnectionsHash %s != %s" % (c3, b["ConnectionsHash"])
|
||||
print("selftest OK:")
|
||||
print(" CompartmentsHash #1:", c1, "== stored")
|
||||
print(" ConnectionsHash #3:", c3, "== stored <- generatable from scratch")
|
||||
print(" DefinitionsHash #2: provisional (verify live vs current GetTrampler).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_selftest()
|
||||
138
reverse/walkerdto_to_blueprint.py
Normal file
138
reverse/walkerdto_to_blueprint.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert a master-server WalkerDto (e.g. ExpeditionDto.Trampler, from GetExpedition /
|
||||
GetExpeditionWalker) into a loadable WalkerBlueprintDto, recomputing the 3 top-level hashes.
|
||||
|
||||
WHY a transform is needed: the WS channel serializes enums as INTEGERS and omits null EpbId on
|
||||
connections; the blueprint/hash ("storage") form uses enum NAME strings and includes EpbId:null.
|
||||
ObjectToByteArray hashes the storage form (verified in trampler_hashes.py). So WS-form -> storage-form
|
||||
before hashing.
|
||||
|
||||
Enum maps (from il2cpp/dump.cs):
|
||||
ConnectionSlotType: DOOR=0 HATCH=1 STRUCTURE=2 BALCONY=3 DECK=4
|
||||
ConnectionState : DEFAULT=0 DOOR=1 OPEN=2 (MasterserverDtos.ConnectionState, 35406)
|
||||
ConnectionsCount : FULL=0 PARTIAL=1 ERROR=2
|
||||
|
||||
Self-verifies offline via round-trip on the known sample (storage->WS->storage is identity and
|
||||
reproduces the stored CompartmentsHash/ConnectionsHash).
|
||||
"""
|
||||
import json, os, sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from trampler_hashes import compartments_hash, connections_hash, definitions_hash
|
||||
|
||||
SLOT = ["DOOR", "HATCH", "STRUCTURE", "BALCONY", "DECK"] # ConnectionSlotType
|
||||
STATE = ["DEFAULT", "DOOR", "OPEN"] # ConnectionState
|
||||
COUNT = ["FULL", "PARTIAL", "ERROR"] # ConnectionsCount
|
||||
SLOT_I = {n: i for i, n in enumerate(SLOT)}
|
||||
STATE_I = {n: i for i, n in enumerate(STATE)}
|
||||
COUNT_I = {n: i for i, n in enumerate(COUNT)}
|
||||
|
||||
|
||||
def _od(*pairs):
|
||||
"""build a dict in explicit (declaration) order."""
|
||||
d = {}
|
||||
for k, v in pairs:
|
||||
d[k] = v
|
||||
return d
|
||||
|
||||
|
||||
def _decor_i2s(decor):
|
||||
"""DecorationsInfo: socket enums int->name (storage form)."""
|
||||
if decor is None:
|
||||
return None
|
||||
socks = []
|
||||
for s in decor.get("Sockets", []):
|
||||
vals = []
|
||||
for kv in s.get("Value", []):
|
||||
v = kv.get("Value") or {}
|
||||
vals.append(_od(("Key", SLOT[kv["Key"]]),
|
||||
("Value", _od(("state", STATE[v["state"]]), ("count", COUNT[v["count"]])))))
|
||||
socks.append(_od(("Key", s["Key"]), ("Value", vals)))
|
||||
return _od(("Sockets", socks))
|
||||
|
||||
|
||||
def _decor_s2i(decor):
|
||||
"""inverse, for round-trip self-test only."""
|
||||
if decor is None:
|
||||
return None
|
||||
socks = []
|
||||
for s in decor.get("Sockets", []):
|
||||
vals = []
|
||||
for kv in s.get("Value", []):
|
||||
v = kv.get("Value") or {}
|
||||
vals.append(_od(("Key", SLOT_I[kv["Key"]]),
|
||||
("Value", _od(("state", STATE_I[v["state"]]), ("count", COUNT_I[v["count"]])))))
|
||||
socks.append(_od(("Key", s["Key"]), ("Value", vals)))
|
||||
return _od(("Sockets", socks))
|
||||
|
||||
|
||||
def comp_i2s(bp):
|
||||
"""one CompartmentBlueprintDto WS->storage (CompartmentBlueprintDto field order)."""
|
||||
return _od(("Id", bp["Id"]), ("EpbId", bp.get("EpbId")), ("CellCoordinate", bp["CellCoordinate"]),
|
||||
("DecorationsInfo", _decor_i2s(bp.get("DecorationsInfo"))), ("Rotation", bp["Rotation"]),
|
||||
("CompartmentHash", bp.get("CompartmentHash")), ("DefinitionHash", bp.get("DefinitionHash")))
|
||||
|
||||
|
||||
def comp_s2i(bp):
|
||||
return _od(("Id", bp["Id"]), ("EpbId", bp.get("EpbId")), ("CellCoordinate", bp["CellCoordinate"]),
|
||||
("DecorationsInfo", _decor_s2i(bp.get("DecorationsInfo"))), ("Rotation", bp["Rotation"]),
|
||||
("CompartmentHash", bp.get("CompartmentHash")), ("DefinitionHash", bp.get("DefinitionHash")))
|
||||
|
||||
|
||||
def conn_i2s(bp):
|
||||
"""one ConnectionBlueprintDto WS->storage: add EpbId:null, enums int->name (declaration order)."""
|
||||
return _od(("Id", bp["Id"]), ("EpbId", bp.get("EpbId")), ("ActualDirection", bp["ActualDirection"]),
|
||||
("GridCoordinate", bp["GridCoordinate"]), ("SlotType", SLOT[bp["SlotType"]]),
|
||||
("State", STATE[bp["State"]]))
|
||||
|
||||
|
||||
def conn_s2i(c):
|
||||
"""inverse: drop EpbId, enums name->int (round-trip self-test only)."""
|
||||
return _od(("Id", c["Id"]), ("ActualDirection", c["ActualDirection"]),
|
||||
("GridCoordinate", c["GridCoordinate"]), ("SlotType", SLOT_I[c["SlotType"]]),
|
||||
("State", STATE_I[c["State"]]))
|
||||
|
||||
|
||||
def walkerdto_to_blueprint(walker, definitions=None, version=1):
|
||||
"""ExpeditionDto.Trampler / WalkerDto -> WalkerBlueprintDto with recomputed hashes."""
|
||||
chassis = comp_i2s(walker["Chassis"]["Blueprint"])
|
||||
comps = [comp_i2s(c["Blueprint"]) for c in walker["Compartments"]]
|
||||
conns = [conn_i2s(c["Blueprint"]) for c in walker["Connections"]]
|
||||
bp = _od(("Id", walker.get("BlueprintId")), ("UniqueId", walker.get("BlueprintUniqueId")),
|
||||
("Version", version), ("Chassis", chassis), ("Compartments", comps), ("Connections", conns),
|
||||
("CompartmentsHash", compartments_hash(comps)), ("DefinitionsHash", ""),
|
||||
("ConnectionsHash", connections_hash(conns)))
|
||||
if definitions is not None:
|
||||
bp["DefinitionsHash"] = definitions_hash(definitions)
|
||||
return bp
|
||||
|
||||
|
||||
def _verify_roundtrip():
|
||||
"""storage -> WS -> storage must be identity (and reproduce stored hashes) on the sample."""
|
||||
p = os.path.join(os.path.dirname(__file__), "..", "extracted", "playfab_titledata_TramplerBlueprint3.json")
|
||||
s = json.load(open(p))
|
||||
comps_ws = [comp_s2i(c) for c in s["Compartments"]]
|
||||
conns_ws = [conn_s2i(c) for c in s["Connections"]]
|
||||
comps_back = [comp_i2s(c) for c in comps_ws]
|
||||
conns_back = [conn_i2s(c) for c in conns_ws]
|
||||
cj = lambda o: json.dumps(o, separators=(",", ":"))
|
||||
assert cj(comps_back) == cj(s["Compartments"]), "compartment round-trip not identity"
|
||||
assert cj(conns_back) == cj(s["Connections"]), "connection round-trip not identity"
|
||||
assert compartments_hash(comps_back) == s["CompartmentsHash"], "CompartmentsHash mismatch"
|
||||
assert connections_hash(conns_back) == s["ConnectionsHash"], "ConnectionsHash mismatch"
|
||||
print("round-trip OK: storage->WS->storage is byte-identical; #1 & #3 hashes reproduce.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_verify_roundtrip()
|
||||
exp = os.path.join(os.path.dirname(__file__), "..", "extracted", "master_GetExpedition.json")
|
||||
if os.path.exists(exp):
|
||||
tr = json.load(open(exp)).get("Trampler")
|
||||
if tr:
|
||||
bp = walkerdto_to_blueprint(tr)
|
||||
out = os.path.join(os.path.dirname(__file__), "..", "extracted", "host_trampler_blueprint.json")
|
||||
json.dump(bp, open(out, "w"), indent=1, ensure_ascii=False)
|
||||
print("converted host trampler -> %s" % out)
|
||||
print(" chassis=%s comps=%d conns=%d" % (bp["Chassis"]["EpbId"], len(bp["Compartments"]), len(bp["Connections"])))
|
||||
print(" CompartmentsHash=%s" % bp["CompartmentsHash"])
|
||||
print(" ConnectionsHash =%s" % bp["ConnectionsHash"])
|
||||
print(" DefinitionsHash =%s (provisional - needs live verify)" % (bp["DefinitionsHash"] or "<empty>"))
|
||||
270
reverse/ws_scrape.py
Normal file
270
reverse/ws_scrape.py
Normal file
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Decode the SAND master-server WebSocket stream out of a packet capture.
|
||||
|
||||
The master server is a .NET `ClientWebSocket` to `ws://<region>.hologryph.com/gameclient/`
|
||||
(port 80, **cleartext** — confirmed from the IL2CPP metadata: only `ws://` literals, no `wss`).
|
||||
Messages are request/response `OperationResult<T>` plus server-push `IClientEvent`s, serialized
|
||||
by the game's `IDataSerializer` (JsonDataSerializer / BsonDataSerializer — JSON is the likely
|
||||
default; this decoder tries JSON, BSON and MessagePack so the exact encoding doesn't block us).
|
||||
|
||||
What it does:
|
||||
1. groups packets into TCP streams, reassembles each direction by sequence number
|
||||
2. finds the WebSocket HTTP upgrade (`GET /gameclient/ ... Upgrade: websocket` / `101`)
|
||||
3. parses RFC-6455 frames (handles masking + continuation), per direction
|
||||
4. decodes each message payload (JSON -> BSON -> MessagePack -> hex) and prints it
|
||||
5. tags messages whose shape matches a known DTO (ServerDto, RegionInfo,
|
||||
CompartmentDefinitionDto, ResearchNode*, OperationResult, IClientEvent...)
|
||||
|
||||
Because it's cleartext, NO MITM/cert is needed for this channel — just capture port 80.
|
||||
|
||||
Usage:
|
||||
venv/bin/python reverse/ws_scrape.py <capture.pcapng> [--port 80] [--host hologryph]
|
||||
[--out extracted/master_ws.json]
|
||||
"""
|
||||
import sys, json, struct, argparse
|
||||
from collections import defaultdict
|
||||
from scapy.all import rdpcap, IP, IPv6, TCP, Raw
|
||||
|
||||
try:
|
||||
import msgpack
|
||||
except Exception:
|
||||
msgpack = None
|
||||
|
||||
# ---- known DTO field-sets, to label decoded objects (from il2cpp/dump.cs) ----
|
||||
DTO_SIGNATURES = {
|
||||
"ServerDto": {"Name", "Description", "UpTime", "Ip", "Port"},
|
||||
"WorldEndpointData": {"worldName", "address", "port", "custom"},
|
||||
"CompartmentDefinitionDto": {"EpbId", "HP", "Weight", "Properties", "CrownPrice"},
|
||||
"ResearchNodeJsonDto": {"Id", "Tier", "ResearchPrice", "RequiredNodesIds", "DependentNodesIds"},
|
||||
"ResearchTreeJsonDto": {"Roots", "Nodes"},
|
||||
"ItemDto": {"DefinitionName", "SellPrice", "Outfitable", "IsLarge"},
|
||||
"ShopItemDto": {"DefinitionName", "BuyPrice", "Amount"},
|
||||
"PriceDto": {"ItemDefinition", "Amount"},
|
||||
"UserDto": {"Id", "DatabaseId"},
|
||||
"OperationResult": {"IsSucceed", "Error", "Status"},
|
||||
"LoginProcessResult": {"IsSuccess", "Error"},
|
||||
}
|
||||
|
||||
|
||||
def label_obj(o, depth=0):
|
||||
"""Best-effort DTO name for a decoded dict (and recurse a little)."""
|
||||
names = []
|
||||
if isinstance(o, dict):
|
||||
keys = set(o.keys())
|
||||
for name, sig in DTO_SIGNATURES.items():
|
||||
if sig <= keys:
|
||||
names.append(name)
|
||||
if depth < 2:
|
||||
for v in o.values():
|
||||
names += label_obj(v, depth + 1)
|
||||
elif isinstance(o, list) and o and depth < 2:
|
||||
names += label_obj(o[0], depth + 1)
|
||||
return names
|
||||
|
||||
|
||||
# ---------- minimal BSON decoder (Newtonsoft.Json.Bson wire format) ----------
|
||||
def _bson_cstring(b, i):
|
||||
j = b.index(0, i)
|
||||
return b[i:j].decode("utf-8", "replace"), j + 1
|
||||
|
||||
|
||||
def _bson_doc(b, i):
|
||||
ln = struct.unpack_from("<i", b, i)[0]
|
||||
end = i + ln
|
||||
i += 4
|
||||
out = {}
|
||||
while i < end - 1:
|
||||
t = b[i]; i += 1
|
||||
name, i = _bson_cstring(b, i)
|
||||
if t == 0x01: # double
|
||||
out[name] = struct.unpack_from("<d", b, i)[0]; i += 8
|
||||
elif t == 0x02: # string
|
||||
sl = struct.unpack_from("<i", b, i)[0]; i += 4
|
||||
out[name] = b[i:i + sl - 1].decode("utf-8", "replace"); i += sl
|
||||
elif t == 0x03: # embedded doc
|
||||
out[name], i = _bson_doc(b, i)
|
||||
elif t == 0x04: # array
|
||||
d, i = _bson_doc(b, i)
|
||||
out[name] = [d[k] for k in sorted(d, key=lambda x: int(x) if x.isdigit() else 0)]
|
||||
elif t == 0x08: # bool
|
||||
out[name] = b[i] != 0; i += 1
|
||||
elif t == 0x0A: # null
|
||||
out[name] = None
|
||||
elif t == 0x10: # int32
|
||||
out[name] = struct.unpack_from("<i", b, i)[0]; i += 4
|
||||
elif t == 0x12: # int64
|
||||
out[name] = struct.unpack_from("<q", b, i)[0]; i += 8
|
||||
else:
|
||||
raise ValueError("bson type 0x%02x" % t)
|
||||
return out, end
|
||||
|
||||
|
||||
def try_decode(payload):
|
||||
"""Return (encoding, obj) or (None, None)."""
|
||||
if not payload:
|
||||
return None, None
|
||||
# JSON (text frames / utf8)
|
||||
s = payload.lstrip()
|
||||
if s[:1] in (b"{", b"[", b'"') or s[:1].isdigit():
|
||||
try:
|
||||
return "json", json.loads(payload.decode("utf-8"))
|
||||
except Exception:
|
||||
pass
|
||||
# BSON (starts with int32 length == len(payload))
|
||||
if len(payload) >= 5:
|
||||
try:
|
||||
ln = struct.unpack_from("<i", payload, 0)[0]
|
||||
if ln == len(payload):
|
||||
obj, _ = _bson_doc(payload, 0)
|
||||
return "bson", obj
|
||||
except Exception:
|
||||
pass
|
||||
# MessagePack
|
||||
if msgpack is not None:
|
||||
try:
|
||||
return "msgpack", msgpack.unpackb(payload, raw=False, strict_map_key=False)
|
||||
except Exception:
|
||||
pass
|
||||
return None, None
|
||||
|
||||
|
||||
# ---------------------- TCP reassembly + WS framing ----------------------
|
||||
def reassemble(packets, port, host_filter):
|
||||
"""Yield (stream_key, client_bytes, server_bytes) for TCP streams on `port`
|
||||
whose handshake Host matches host_filter (substring, case-insensitive)."""
|
||||
flows = defaultdict(lambda: {"a": {}, "b": {}, "ips": None})
|
||||
for p in packets:
|
||||
if not p.haslayer(TCP):
|
||||
continue
|
||||
ipl = p[IP] if p.haslayer(IP) else (p[IPv6] if p.haslayer(IPv6) else None)
|
||||
if ipl is None:
|
||||
continue
|
||||
t = p[TCP]
|
||||
if t.dport != port and t.sport != port:
|
||||
continue
|
||||
a = (ipl.src, t.sport)
|
||||
b = (ipl.dst, t.dport)
|
||||
key = tuple(sorted([a, b]))
|
||||
f = flows[key]
|
||||
f["ips"] = key
|
||||
side = "a" if (a == key[0]) else "b"
|
||||
if p.haslayer(Raw):
|
||||
f[side][t.seq] = bytes(p[Raw].load)
|
||||
|
||||
for key, f in flows.items():
|
||||
# the side whose endpoint port == `port` is the server
|
||||
(e0, e1) = key
|
||||
server_side = "a" if e0[1] == port else "b"
|
||||
client_side = "b" if server_side == "a" else "a"
|
||||
cbytes = b"".join(f[client_side][s] for s in sorted(f[client_side]))
|
||||
sbytes = b"".join(f[server_side][s] for s in sorted(f[server_side]))
|
||||
# require a WS handshake, optionally host-filtered
|
||||
if b"Upgrade: websocket" not in cbytes and b"upgrade: websocket" not in cbytes:
|
||||
continue
|
||||
if host_filter:
|
||||
hdr = cbytes[:cbytes.find(b"\r\n\r\n") + 4].lower()
|
||||
if host_filter.lower().encode() not in hdr:
|
||||
continue
|
||||
yield key, cbytes, sbytes
|
||||
|
||||
|
||||
def ws_frames(stream):
|
||||
"""Parse RFC-6455 frames from a byte stream that starts AFTER the HTTP handshake.
|
||||
Yields (opcode, payload_bytes). Reassembles continuation frames."""
|
||||
i = stream.find(b"\r\n\r\n")
|
||||
i = i + 4 if i >= 0 else 0
|
||||
b = stream
|
||||
n = len(b)
|
||||
cur_op = None
|
||||
cur = bytearray()
|
||||
while i + 2 <= n:
|
||||
b0 = b[i]; b1 = b[i + 1]
|
||||
fin = b0 & 0x80
|
||||
op = b0 & 0x0F
|
||||
masked = b1 & 0x80
|
||||
ln = b1 & 0x7F
|
||||
i += 2
|
||||
if ln == 126:
|
||||
if i + 2 > n:
|
||||
break
|
||||
ln = struct.unpack_from(">H", b, i)[0]; i += 2
|
||||
elif ln == 127:
|
||||
if i + 8 > n:
|
||||
break
|
||||
ln = struct.unpack_from(">Q", b, i)[0]; i += 8
|
||||
mask = b""
|
||||
if masked:
|
||||
if i + 4 > n:
|
||||
break
|
||||
mask = b[i:i + 4]; i += 4
|
||||
if i + ln > n:
|
||||
break
|
||||
data = bytearray(b[i:i + ln]); i += ln
|
||||
if masked:
|
||||
for k in range(len(data)):
|
||||
data[k] ^= mask[k & 3]
|
||||
if op == 0x8: # close
|
||||
break
|
||||
if op in (0x9, 0xA): # ping/pong
|
||||
continue
|
||||
if op == 0x0: # continuation
|
||||
cur += data
|
||||
else:
|
||||
if cur_op is not None:
|
||||
pass
|
||||
cur_op = op
|
||||
cur = bytearray(data)
|
||||
if fin and cur_op in (0x1, 0x2):
|
||||
yield cur_op, bytes(cur)
|
||||
cur_op = None
|
||||
cur = bytearray()
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("pcap")
|
||||
ap.add_argument("--port", type=int, default=80)
|
||||
ap.add_argument("--host", default="hologryph", help="handshake Host substring filter ('' = any)")
|
||||
ap.add_argument("--out", default=None)
|
||||
args = ap.parse_args()
|
||||
|
||||
pk = rdpcap(args.pcap)
|
||||
messages = []
|
||||
nstreams = 0
|
||||
for key, cbytes, sbytes in reassemble(pk, args.port, args.host):
|
||||
nstreams += 1
|
||||
# show the handshake request line + host
|
||||
line0 = cbytes.split(b"\r\n", 1)[0].decode(errors="replace")
|
||||
host = ""
|
||||
for ln in cbytes[:cbytes.find(b"\r\n\r\n") + 4].split(b"\r\n"):
|
||||
if ln.lower().startswith(b"host:"):
|
||||
host = ln.decode(errors="replace")
|
||||
print("### WS stream %s %s %s" % (key, line0, host))
|
||||
for direction, stream in (("S->C", sbytes), ("C->S", cbytes)):
|
||||
for op, payload in ws_frames(stream):
|
||||
enc, obj = try_decode(payload)
|
||||
rec = {"stream": str(key), "dir": direction,
|
||||
"opcode": ("text" if op == 1 else "binary"),
|
||||
"encoding": enc, "len": len(payload)}
|
||||
if obj is not None:
|
||||
rec["dto"] = sorted(set(label_obj(obj)))
|
||||
rec["data"] = obj
|
||||
else:
|
||||
rec["raw_prefix"] = payload[:48].hex()
|
||||
messages.append(rec)
|
||||
tag = ("[" + ",".join(rec.get("dto", [])) + "] ") if rec.get("dto") else ""
|
||||
preview = json.dumps(obj, default=str)[:160] if obj is not None else rec["raw_prefix"]
|
||||
print(" %s %-7s %-8s %s%s" % (direction, rec["opcode"], enc, tag, preview))
|
||||
|
||||
print("\n%d WS stream(s), %d message(s)" % (nstreams, len(messages)))
|
||||
if nstreams == 0:
|
||||
print("No WebSocket streams found. If the backend was online, check --port/--host, "
|
||||
"or the capture may not span the master-server connection.")
|
||||
if args.out:
|
||||
json.dump({"_source": args.pcap, "messages": messages}, open(args.out, "w"),
|
||||
indent=1, default=str, ensure_ascii=False)
|
||||
print("wrote", args.out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user