# GTA:SA model viewer Drop a `.dff` and `.txd` into `models/`, open `/samp/skin/`, and it renders. The RenderWare files are parsed in the browser — no conversion step, no Blender, no build tooling. `/samp/skin/` is the only thing the server exposes. The root, `/index.html` and any other path return 404. ## Running ```bash npm install npm start ``` Then open . `PORT` and `MODEL_ROOT` are read from the environment, so you can point it at a folder outside the project: ```bash MODEL_ROOT=C:/samp/skins npm start ``` See [.env.example](.env.example) for the full set, including the S3 options. ## Getting the default SA-MP skins The 312 SA-MP skin ids are just GTA:SA ped model ids, so they come out of your own install rather than off the web: ```bash node tools/extract-peds.js --game "D:/SteamLibrary/steamapps/common/Grand Theft Auto San Andreas" ``` It reads `data/peds.ide` for the id -> model mapping, pulls each `.dff` and `.txd` out of `models/gta3.img` and `models/player.img`, and writes them to `models/.dff` / `models/.txd`. `--game` is optional if the install is in one of the usual places. That yields **265 skins**. Three groups are absent for real reasons: - The gaps (1-6, 8, 42, 65, 74, 86, 119, 149, 208, 265-273, 289+) are the ids SA-MP treats as invalid. They are simply not in peds.ide. - Ids 290-299 (`special01`-`special10`) are empty placeholder slots that servers fill with `AddCharModel`, so there is nothing to extract. - Id 0 (CJ) has no single model. `player.dff` is an 8KB stub and the game assembles him from the clothing components in player.img at runtime, so the extractor skips him rather than writing a placeholder. Extracted models are gitignored; they're Rockstar's assets, not yours to ship. ## Naming Files are named by id: `.dff` and `.txd`, in `models/` locally or under the bucket prefix on S3. The `.txd` is optional; without it the model renders untextured, and a `.txd` with no matching `.dff` is ignored. You can drag a `.dff` and `.txd` straight onto the page to preview them without filing them first, which is handy for checking a custom skin. ## Deployment The server reads its model list from a local folder by default, or from S3 when `S3_BUCKET` is set. Either way the list is built once at boot and refreshed on a timer (`INDEX_REFRESH_MS`, default 15 minutes), so uploading a new skin does not need a restart. `MODELS_BASE_URL` controls where the browser fetches model files from: - **unset** — files are requested from `/models` on this origin, and nginx proxies that to S3 or CloudFront. Same-origin, so no bucket CORS rule. - **set to a CDN origin** — the browser fetches S3 directly and the server stays out of the data path. Needs the CORS rule in [deploy/bucket-cors.json](deploy/bucket-cors.json). Ready-to-edit configs live in [deploy/](deploy/): an nginx site that exposes only `/samp/skin/`, a hardened systemd unit, the bucket CORS rule and a read-only IAM policy. If you would rather not put AWS credentials on the box at all, drop the `@aws-sdk/client-s3` dependency and have `/api/skin/:id` return the URLs without checking the index. You lose clean 404s for unknown ids — the viewer reports the failed fetch instead. ## API - `GET /api/skin/:id` — the URLs for one id, or 404 ## What's implemented `public/js/rw/` is a standalone RenderWare reader with no dependencies: - `stream.js` — chunk walker. Every RW chunk is a 12 byte header (`type`, `size`, `libraryID`) followed by its body; containers just hold more chunks. Both formats below are built on this. - `dff.js` — clump, frame hierarchy, geometry (positions, normals, UVs, prelit colours, triangles), material list and texture names. - `txd.js` — texture dictionary, D3D8 and D3D9 rasters. - `dxt.js` — DXT1/3/5 decoder, everything ends up as RGBA8. Verified against 179 dffs and 127 txds (1383 textures) from a SA-MP cache: all parsed, with every triangle index and material id in range. Things deliberately left out, in rough order of how likely they are to matter: - **Animation.** Ped dffs store vertices in bind pose, so they render standing still, correctly. Posing them would need the Skin PLG (`0x116`) and HAnim PLG (`0x11E`) chunks turned into a `THREE.Skeleton` plus an IFP reader. Bind pose has one non-obvious rule: a *skinned* geometry's vertices live in the space of its atomic frame's **parent**, not the atomic frame itself, so that one frame is skipped when placing the mesh. The atomic hangs off `Pelvis` in most peds but off `Root` in a handful, which is why neither "always apply the frame" nor "always use identity" works. Checked against all 265 extracted peds: every one stands upright at human height under this rule. - **Native geometry.** PS2/Xbox dffs use a different vertex layout and are rejected with a message rather than parsed. PC files are fine. - **Bin Mesh PLG.** SA stores per-material tristrips in `0x50E`, but the plain triangle list is always present too, so the viewer groups that by material instead. - Environment/specular maps, UV animation, vehicle paint colours, IMG archives.