Compare commits

..

11 Commits

Author SHA1 Message Date
DownloadPizza
dabdf51383 walker naming convention in CLAUDE.md (first/secondNameIndex 0-31 via name_index.json); add Veteran Beast copy 2026-06-16 18:33:02 +02:00
DownloadPizza
ec1295e39c weapon damage: confirm Damage*DataComponents absent from item/ammo bundles (runtime/server-only); rank routes to base numbers 2026-06-16 17:51:40 +02:00
DownloadPizza
f1dbc06425 docs: note the symbol-injected Ghidra DB is built & ready (ghidra/project/SAND, 564k methods) 2026-06-16 16:38:22 +02:00
DownloadPizza
43cc032e02 ghidra: track apply_il2cpp_symbols.py master in reverse/ (ghidra/ is gitignored) 2026-06-16 15:52:06 +02:00
DownloadPizza
e390461a53 ghidra: symbol-inject workflow (Il2CppDumper script.json) instead of full auto-analysis
- Full auto-analysis of the 137MB IL2CPP GameAssembly.dll is the wrong default:
  Decompiler Parameter ID is ~single-threaded, ran 5h+ with no checkpoint/ETA
  and saves only at the end. It rediscovers what Il2CppDumper already knows.
- Add ghidra/scripts/apply_il2cpp_symbols.py: headless-adapted port of
  yoten/ghidra.py (askFile -> script arg) that imports the dumper's script.json
  symbol table (function boundaries + names + string/metadata labels) onto a
  -noanalysis import. Names-only/light path; struct+signature path documented.
- docs/GHIDRA.md: full workflow, address convention (base.add(Address), no -0x1000),
  the _JAVA_OPTIONS=-Xmx4g heap-cap gotcha, targeted decomp/disasm commands.
2026-06-16 15:51:00 +02:00
DownloadPizza
f049db6921 gitignore + untrack extracted/ (kept on disk); master-server GetExpedition op
- .gitignore: ignore /extracted/ (regenerable game data) and /tmp/ (scratch)
- git rm --cached the 38 extracted/ files: untracked but left on disk, not deleted
- master_scrape.py: add GetExpedition=7 to ACTIONS (was missing; pulls ExpeditionDto.Trampler)
- docs: mark master-server /connect blocker cleared 2026-06-16 (server back up);
  server-side upsert hash validation remains untested (live re-test not yet run)
2026-06-16 13:45:55 +02:00
DownloadPizza
608ea295a0 trampler: note server-side upsert hash validation as open/untested (blocked by /connect NRE) 2026-06-16 09:50:58 +02:00
DownloadPizza
bd01d6753a trampler: hash lifecycle RE'd via live round-trip (client recomputes all 5 on save)
- All 5 walker hashes are recomputed client-side on save from the local
  CompartmentsDatabase + CompartmentDefinitionDto (same build => byte-identical
  hashes; no per-walker secret). Wiping any/all hashes loads/opens/edits fine;
  one in-editor save regenerates everything (+ new file UUID & UniqueId).
- VERSION flag (WalkerBlueprintContainer.ValidateVersion) depends only on the
  structural Compartments/Connections hashes, not the Definition hashes; ERRORS
  is a separate structural check. Hashes = integrity/version-staleness markers,
  not security. Server upsert validation still untested.
- Document the 5-hash table (3 top-level + 2 per-part) and offline-computability
  in docs/TRAMPLER.md + CLAUDE.md; include experiment artifacts + baseline.
2026-06-16 09:48:50 +02:00
DownloadPizza
aa7425221f docs: add CLAUDE.md (tools, subcommands, architecture, .wbt format/hashes) 2026-06-16 09:05:37 +02:00
DownloadPizza
fc6b270fa8 master-server replay + trampler RE: protocol, hashes, footprints, map renderer
- master_scrape.py: live master-server (ger.hologryph.com) ClientMessage replay over the
  two-socket /login + /connect handshake (PlayFab ticket auth). Pulled compartment defs,
  shop prices, research tree, storage, characters, expedition -> extracted/master_*.json
- PlayFab confirmed auth-only for this title (Economy disabled); docs corrected
- trampler_hashes.py: blueprint hash algo MD5(UTF8(compact-JSON)); CompartmentsHash(#1) and
  ConnectionsHash(#3) verified & generatable from scratch
- walkerdto_to_blueprint.py: WalkerDto(expedition) -> WalkerBlueprintDto, enum int<->name,
  verified by storage->WS->storage round-trip
- render_trampler.py: per-floor map from CompartmentsDatabase cell footprints (rotation solved
  via overlap check) + doors/hatches from Connections + turret arcs + cargo C1-C8 in game order
- docs/MASTER_SERVER.md, docs/TRAMPLER.md; ghidra address-offset bug fixed (no -0x1000)
2026-06-16 00:35:17 +02:00
DownloadPizza
3df0797acc scrape tooling: live capture triage + master-server WS decoder + PlayFab REST scraper
Built and unit-tested ahead of a live playtest window:
- reverse/capture_hosts.py: pcap -> DNS/SNI/endpoints in order; extracts PlayFab TitleId,
  flags hologryph master-server region + config CDN.
- reverse/ws_scrape.py: TCP reassembly + RFC-6455 framing for the cleartext ws://<region>.
  hologryph.com/gameclient/ stream; decodes JSON/BSON/MessagePack; auto-labels ServerDto,
  CompartmentDefinitionDto, ResearchNodeJsonDto, OperationResult, etc. No MITM needed.
- reverse/playfab_scrape.py: LoginWithSteam (or captured EntityToken) -> Catalog/SearchItems
  (+ Inventory/TitleData); prices resolved to item names. Read-only.
- docs/SCRAPE_RUNBOOK.md: turnkey steps for when servers are online.
2026-06-12 10:06:48 +02:00
34 changed files with 2011 additions and 58438 deletions

5
.gitignore vendored
View File

@@ -43,3 +43,8 @@ dist/
# Environment # Environment
.env .env
.env.* .env.*
/reverse/.secrets/
# Extracted game data + scratch output (regenerable; kept locally, not tracked)
/extracted/
/tmp/

170
CLAUDE.md Normal file
View 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, **031 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 031) · `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).

View File

@@ -1,5 +1,14 @@
# Backend = PlayFab (Azure) — acquiring server-authoritative data # 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) ## Observed boot behavior — no active playtest (GROUND TRUTH, reported by user)
When no playtest is running, the exact, observed sequence is: When no playtest is running, the exact, observed sequence is:

96
docs/GHIDRA.md Normal file
View 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
View 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
View 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
View 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 C1C8 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 **C1C8** 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).

View File

@@ -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 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. **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.30.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) ## 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 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

View File

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

View File

@@ -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"
]
}
}
}

View File

@@ -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
}
]
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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