), progress , speed kept `. Checked in game against the east wall on the main pipe south lip: a 57 unit run up from x 122 let go 4.83 short at the full grind speed of 19.5, with the player alive at full health.
* **Not coped.** The two wooden kickers near (45, 3.3, 47) and (45, 3.3, 62), the taxi funbox plywood near (120 to 137, 2 to 5, 75 to 83), the three earlier mesh rails, and the vertical-faced tops at heights 45 to 51, which have no ramp face.
* **Checked in game.** Dropped onto the main pipe lips, the north tier, the south wall and the pocket, the player grinds with sparks on the coping, and from the north lip the grind carries onto the west diagonal. The harness injects no gameplay input, so a drop only ever grinds west, toward a coping rail’s progress 0 end. An eastbound grind runs from a scratch copy of the level with the two main-pipe coping rails rotated 180 degrees (`.tools/evidence/thps-east-wall-scratch.mblevel.json`), which turns the same catch eastbound at the game’s own grind speed. Ollies off the lips, and grinding from a run up a pipe, still need real keyboard input.
* **Slide-gated.** Because these rails sit on every lip, the level sets `grindRequiresSlide`, so a grind starts only while the slide button is held and riding over a coping keeps the player’s momentum (`GrindGate` and `GrindGatePatch`, see “Grind gate” in [development.md](https://github.com/pythoninthegrass/megabonk_mods/blob/main/docs/development.md)). Checked in game on the main pipe’s north coping and the north wall tier with the harness holding the button in place of the keyboard. Released, the catch is refused and the player stands on the lip normally: `GrindGate: slide released, grind start blocked at (140.0, 7.9, 8.6) by the StartRail hook.` on the coping, `... blocked at (160.0, 11.1, -27.1) ...` on the tier (`.tools/evidence/grind-gate-live-blocked.png`). Held, the same drop starts a real moving grind: `GrindGate: slide held, grind started at (140.0, 7.9, 8.6) on the rail at (149.4, 5.4, 8.6).` on the coping, where the player travels to x 121, and `GrindGate: slide held, grind started at (160.0, 10.6, -27.1) on the rail at (159.2, 8.3, -27.1).` on the tier, where the run covers x 160 to 118.9 before the rail’s end (`grind-gate-tier-held.png`). Chaining asks the same question, held: `RailHandoff: moved onto rail at distance 0.00, progress 0.000, direction 1.0.` onto the west diagonal, which then runs to `progress 1.000`. The button is a hold rather than a toggle, so releasing it at any point comes the player off the rail: `GrindGate: slide released, the grind ends at (112.2, 5.7, 6.6), progress 0.397.` mid-diagonal, with momentum carrying them the rest of the way along the lip while the catch keeps refusing them (`... blocked at (102.5, 3.1, 4.1) by the StartRail hook.`) until the button goes down again, when that same rail is caught at once (`GrindGate: slide held, grind started at (102.5, 3.1, 4.1) on the rail at (110.0, 3.0, 6.1).`). Checked on the keyboard as well: holding slide catches the coping and letting go drops the player off the lip. While held, `GrindAssist` also catches a rail within 2 units of the player’s feet, so an approach that misses the bar still goes into a grind: dropped 1.6 units off that bar, `GrindAssist: caught the rail at (149.4, 5.4, 8.6) from 0.00 units at (150.0, 9.4, 10.2), progress 0.510.` That every blocked line names the `StartRail` hook answers what the dump could not: the game’s `Rail.FixedUpdate` catch calls `StartRail` without consulting `CanStartGrind`. As a control, the same level with the key stripped (`.tools/evidence/thps-no-gate-scratch.mblevel.json`) still grinds on contact: dropped at x 176 with the button on the real device and nobody near the keyboard, the player rode the whole north coping and on to the floor at (88.6, 2.4, 0.5) with no `GrindGate` line at all (`.tools/evidence/grind-gate-no-key-control.png`).
### Slope limit on the half pipes
[Section titled “Slope limit on the half pipes”](#slope-limit-on-the-half-pipes)
The player could not ollie off the main half pipe edges near (142, -3, 32). The cause is the movement limit, not the collision mesh: the pipe face passes 45 degrees about 3 units up its 8 unit rise, so above that the player is not grounded and cannot ride to the lip. The level file therefore sets `maxSlopeAngle` (`MAX_SLOPE_ANGLE` in `scripts/build_thps_level.py`, 88), which the mod applies to `PlayerMovement.maxSlopeAngle` for the run only (see `level-format.md`); Forest and every level without the key keep the game’s 45. Vertical faces (90 degrees) stay unclimbable, so walls still block. The mod logs `LiveEdit: max slope angle 45 -> 88` when it applies. Open: how the raised limit feels on the other ramps and whether a lower value rides the pipes as well.
### Check against the reference frames
[Section titled “Check against the reference frames”](#check-against-the-reference-frames)
The frames in `tools/thps_warehouse_frames/` come from the 2020 remake, so the textures and lighting differ from the PS1 mesh by design. Compared with frames 4, 12 and 138 and the items map:
* Present: the walled room, the main half pipe and its hanging box, the banks, decks and ledges, the rail bars, the windows and the boss portal.
* Roof: the pack adds a flat dark roof (see “Warehouse roof” in asset-bundles.md), so a view from above the room shows the roof and not the interior; the items map and frame 12 are cut-away or interior views.
* Lighting: the level uses Forest’s fog and lighting, which tints the room teal and has none of the remake’s window light shafts. Surfaces read grey-green and dark.
* Not compared: a pitch-45 south-east overview matching the items map, because of the roof.
# Warehouse construction piece inventory
This is the construction kit’s source partition, not its palette asset list. The builder maps source instances to reusable prefabs through `catalogAsset` in `pieces.json`; source IDs below are not necessarily loadable catalog IDs. The shipped `bundle:thps/Warehouse`, its assets and `levels/thps.mblevel.json` remain unchanged. See [asset-bundles.md](https://github.com/pythoninthegrass/megabonk_mods/blob/main/docs/asset-bundles.md) for the piece prefabs, their pivots and the `pieces.json` sidecar, and the [reference](/docs/thps-warehouse/), [shipped layout](/docs/thps-warehouse-layout/) and [preservation check](https://github.com/pythoninthegrass/megabonk_mods/blob/main/docs/development.md#preserving-the-shipped-warehouse).
## Source and addressing
[Section titled “Source and addressing”](#source-and-addressing)
Source: `.tools/thps1/warehouse/warehouse.obj`, from the PS1 extraction. Source SHA-256: `917aa268b447df4e7131cac0b55cbf0894e848789c86289daf845d25a562b25a`
There are **252 object declarations, 200 nonempty objects and 1,345 polygon faces**, assigned to **214 named source pieces** below. A face number is the one-based ordinal of an `f` line across the whole file, before triangulation, coplanar separation or importer filtering. Ranges are inclusive; comma-separated ranges form one piece. Groups are the literal `o` hashes, not material hashes. Every source face has exactly one owner, including double-sided faces and suppressed post remnants. There is no catch-all remainder. A source row may yield multiple independently editable instances when it contains disconnected sign copies; `sourceId` records their common inventory owner. Module types describe reuse, not a second face assignment.
Empty objects: `947969A2`, `0D703818`, `7A77088E`, `E4139D2D`, `9314ADBB`, `0A1DFC01`, `7D1ACC97`, `0108955F`, `9801C4E5`, `EF06F473`, `B31D819B`, `D0B9495C`, `2A14D021`, `49B018E6`, `BE5B0A8C`, `27525B36`, `50556BA0`, `CE31FE03`, `DFB35C91`, `46BA0D2B`, `6F12583B`, `954EED36`, `F61B0981`, `811C3917`, `1F78ACB4`, `687F9C22`, `1E575FE1`, `875E0E5B`, `370A0C19`, `AE035DA3`, `271B3BAD`, `D9046D35`, `BA664D03`, `CABEDA1B`, `4760F896`, `9F9D74DB`, `86B0FA50`, `0C47BC8C`, `EF5B1379`, `817474C9`, `27CBD007`, `187D2573`, `F673445F`, `6FD0D543`, `0DE57681`, `F28254C7`, `A114B35C`, `94EC273B`, `7B408C1A`, `E52419B9`, `F0593ECD`, `6E3DAB6E`
Those 52 declarations have no geometry and need no palette placeholder. They are accounted for separately, not assigned invented faces.
Run `uv run scripts/check_thps_piece_inventory.py` to verify the fingerprint, empty declarations, selectors, unique names/IDs and exact face coverage. The same check runs in pytest and fails rather than skips without the extraction. The Markdown tables are the machine-readable inventory; edit them rather than maintaining a competing manifest.
### Coordinates and preservation rules
[Section titled “Coordinates and preservation rules”](#coordinates-and-preservation-rules)
Measurements here are **Megabonk world units**, not the reference video’s estimated metres or its map headings. Transform raw OBJ `(x,y,z)` to `((x-1.519043)*79.2+100, -y*79.2+0.5, (z-1.9661865)*79.2)`. World north is +Z. Keep unrounded source coordinates/UVs in meshes; numbers below are rounded for reading, never snapping targets.
Use one canonical pivot at the midpoint of a module’s lower riding edge, with local +Y up and +Z pointing from toe toward lip; deck/flat pivots are their walking-surface centres. Corner pivots are at the intersection of the extended lip lines at deck height. Wall/window pivots use bottom-centre of the panel. Reusable signs/props/kicker parts use canonical geometry frames; their full sidecar transforms, rather than a source location or yaw alone, define the placement. Roof sections retain the Warehouse axes and use their geometry centre. Store the inverse pivot transform so reassembly restores the original world vertices; standardized loose planks retain their fitted bounds/artwork rather than incidental source skew. These are kit conventions, not changes to the legacy mesh’s centre pivot.
The start area contains paired state/double-sided geometry that the importer currently draws together. Rows naming two groups retain both, including nonidentical material variants; do not deduplicate them on the assumption that only one should be visible. Preserve global `CoplanarLayering` offsets before partitioning so posters and planks do not gain different offsets when their neighbours move. Triangulation, winding, UVs and transparency stay as in `ThpsPack.cs`.
## Source face inventory
[Section titled “Source face inventory”](#source-face-inventory)
### Hanging glass box
[Section titled “Hanging glass box”](#hanging-glass-box)
The room occupies x 149.36–178.95, z 20.93–31.47. Its window objects are individual **window sections**, not every small pane painted in a texture. Each two-face window is the two sides of one section and moves as one piece. The east wall is part of the arena wall panel below, not an eighth glass window.
| ID | Element | Groups | Faces | Module |
| ---------------------------- | ------------------------------------ | -------- | ------- | ------- |
| piece-room-wall-south | Glass box south wall frame | B22D8FBF | 71-82 | wall |
| piece-room-wall-north | Glass box north wall frame | 5C23EE93 | 83-94 | wall |
| piece-room-roof | Glass box roof slab | 4E3C5402 | 177-180 | roof |
| piece-room-floor | Glass box floor slab and edge fascia | 4E3C5402 | 181-201 | deck |
| piece-room-window-se | Glass box south east intact window | BB7DC2A2 | 202-203 | window |
| piece-room-window-sw | Glass box south west broken window | BC1006BB | 204-205 | window |
| piece-room-window-ne | Glass box north east broken window | 521E6797 | 206-207 | window |
| piece-room-window-nw | Glass box north west intact window | CC7AF234 | 208-209 | window |
| piece-room-underside-box | Glass box underside support box | 105A150D | 210-228 | support |
| piece-room-wall-west | Glass box west wall frame | 2B24DE05 | 312-323 | wall |
| piece-room-window-west | Glass box west broken window | 25195701 | 553-554 | window |
| piece-room-chain-north | Glass box north suspension chain | 10500CBD | 121-124 | support |
| piece-room-chain-south | Glass box south suspension chain | 89595D07 | 125-128 | support |
| piece-room-brace-north-outer | Glass box north outer diagonal brace | 6BA45667 | 115-117 | support |
| piece-room-brace-south-outer | Glass box south outer diagonal brace | 1CA366F1 | 118-120 | support |
Floor top is 15.640, bottom 15.311; roof underside is 25.424, top 25.753. Keep each slab’s top, bottom and edge faces together. Intact room windows (`F2184063`) are collision-open only in the authored room bounds; broken windows (`FA88F30D`) are always collision-open. Their frames remain solid. Window pieces must carry that result locally, not keep testing the old world bounds after movement. No new breakable-window gameplay is implied.
### Main half pipe and hip ends
[Section titled “Main half pipe and hip ends”](#main-half-pipe-and-hip-ends)
| ID | Element | Groups | Faces | Module |
| ------------------------- | ---------------------------------------------- | -------- | --------- | --------------- |
| piece-main-hip-north | Main half pipe north west hip end | ED3C7CD2 | 229-231 | hip |
| piece-main-hip-south | Main half pipe south west hip end | 9A3B4C44 | 232-234 | hip |
| piece-main-hip-apron | Main half pipe tapered west sloping apron | 1D44E8A6 | 264-270 | hip-apron |
| piece-main-bank-north-01 | Main half pipe north outer bank west strip | 1F4E66D8 | 330 | outer-bank |
| piece-main-bank-north-02 | Main half pipe north outer bank middle strip | 1F4E66D8 | 331 | outer-bank |
| piece-main-bank-north-03 | Main half pipe north outer bank east strip | 1F4E66D8 | 332 | outer-bank |
| piece-main-bank-north-end | Main half pipe north outer bank hip end | 1F4E66D8 | 333 | bank-end |
| piece-main-bank-south-01 | Main half pipe south outer bank west strip | 86473762 | 334 | outer-bank |
| piece-main-bank-south-02 | Main half pipe south outer bank middle strip | 86473762 | 335 | outer-bank |
| piece-main-bank-south-03 | Main half pipe south outer bank ledge join | 86473762 | 336-338 | outer-bank |
| piece-main-bank-south-end | Main half pipe south outer bank hip end | 86473762 | 339 | bank-end |
| piece-main-south-03 | Main half pipe south transition east section | 03321DFE | 1127-1131 | main-transition |
| piece-main-north-01 | Main half pipe north transition west section | 0A8451D5 | 1132-1136 | main-transition |
| piece-main-south-01 | Main half pipe south transition west section | 7358E971 | 1137-1141 | main-transition |
| piece-main-south-02 | Main half pipe south transition middle section | EA51B8CB | 1142-1146 | main-transition |
| piece-main-north-02 | Main half pipe north transition middle section | 9D56885D | 1147-1151 | main-transition |
| piece-main-flat | Main half pipe rectangular flat bottom | 7D836143 | 1152-1154 | flat |
| piece-main-north-03 | Main half pipe north transition east section | 74352D68 | 1155-1159 | main-transition |
### Wall quarter pipes, corners and decks
[Section titled “Wall quarter pipes, corners and decks”](#wall-quarter-pipes-corners-and-decks)
Deck faces are split from riding transitions even when the original object mixed them. Lip/coping strips stay with their transition; grind paths follow their owning transition, not the adjacent deck. Names use measured world location rather than uncertain remake wall names.
| ID | Element | Groups | Faces | Module |
| ------------------------------ | --------------------------------------------- | -------- | --------------- | --------------- |
| piece-south-transition-end | South wall quarter pipe west end section | C02D64C7 | 272-276 | wall-transition |
| piece-south-deck-end | South wall quarter pipe west end deck | C02D64C7 | 277 | deck |
| piece-pocket-return | South pocket return corner transition | B72A5451 | 278-280,285-296 | corner |
| piece-pocket-return-deck | South pocket return corner deck | B72A5451 | 281-284,297 | deck |
| piece-pocket-tip-west | South pocket rounded tip west transition | 3055F0B3 | 1031-1040 | convex-tip |
| piece-pocket-tip-west-deck | South pocket rounded tip west deck | 3055F0B3 | 1041-1042 | deck |
| piece-pocket-side-west | South pocket west side transition | 4752C025 | 1043-1057 | wall-transition |
| piece-pocket-side-west-deck | South pocket west side deck | 4752C025 | 1058-1062 | deck |
| piece-pocket-tip-east | South pocket rounded tip east transition | A95CA109 | 1063-1072 | convex-tip |
| piece-pocket-tip-east-deck | South pocket rounded tip east deck | A95CA109 | 1073-1074 | deck |
| piece-pocket-side-east | South pocket east side transition | 373834AA | 1075-1079 | wall-transition |
| piece-pocket-side-east-deck | South pocket east side deck | 373834AA | 1080-1081 | deck |
| piece-south-transition-middle | South wall quarter pipe middle run | D9365586 | 1082-1086 | wall-transition |
| piece-south-deck-middle | South wall quarter pipe middle deck | D9365586 | 1087 | deck |
| piece-southeast-corner-deck | South east quarter pipe corner deck | AE316510 | 1088-1092 | deck |
| piece-southeast-corner | South east quarter pipe corner transition | AE316510 | 1093-1107 | corner |
| piece-north-transition-east | North wall quarter pipe east run | 5E49F164 | 1160-1169 | wall-transition |
| piece-north-deck-east | North wall quarter pipe east deck | 5E49F164 | 1170-1171 | deck |
| piece-north-transition-middle | North wall quarter pipe middle run | C740A0DE | 1172-1176 | wall-transition |
| piece-north-deck-middle | North wall quarter pipe middle deck | C740A0DE | 1177 | deck |
| piece-north-upper-transition | North wall upper tier transition | 2E2305EB | 1178-1187 | wall-transition |
| piece-north-upper-deck | North wall upper tier deck | 2E2305EB | 1188-1189 | deck |
| piece-north-transition-west-01 | North wall lower tier west transition | B0479048 | 1190-1194 | wall-transition |
| piece-north-transition-west-02 | North wall lower tier east transition | B0479048 | 1195-1199 | wall-transition |
| piece-north-tier-platform | North wall tier connecting platform | BAD88457 | 1200-1204 | deck |
| piece-south-transition-east | South wall quarter pipe east straight run | 279549C0 | 1205-1209 | wall-transition |
| piece-south-deck-east | South wall quarter pipe east straight deck | 279549C0 | 1210 | deck |
| piece-east-transition | East wall quarter pipe straight run | 50927956 | 1212-1218 | wall-transition |
| piece-east-deck | East wall quarter pipe straight deck | 50927956 | 1211 | deck |
| piece-south-transition-west | South wall quarter pipe west straight run | DE5B919F | 1246-1250 | wall-transition |
| piece-south-deck-west | South wall quarter pipe west straight deck | DE5B919F | 1251 | deck |
| piece-south-transition-return | South wall quarter pipe pocket to middle run | 403F043C | 1252-1256 | wall-transition |
| piece-south-deck-return | South wall quarter pipe pocket to middle deck | 403F043C | 1257 | deck |
### Start platform, roll-ins and upper enclosure
[Section titled “Start platform, roll-ins and upper enclosure”](#start-platform-roll-ins-and-upper-enclosure)
The two roll-ins are separate left/right modules. Start flat-bottom geometry belongs to the platform, not the long roll-in. The high rear quarter pipe has the same 8.06 rise as a wall pipe but starts at y 27.78.
| ID | Element | Groups | Faces | Module |
| ----------------------------- | -------------------------------------------- | ------------------ | --------------- | --------------- |
| piece-start-fence-west | Start roll-in west lattice side panel | 9D14F7B5, 041DA60F | 1-23,48-70 | wall |
| piece-start-gable | Start rear wall and overhead lintel assembly | EA147B7B, ADCB734A | 389-426,723-760 | wall |
| piece-start-window-frame-west | Start window west frame | 602D8163, FE56F714 | 427-432,707-712 | wall |
| piece-start-window-frame-east | Start window east frame | F924D0D9, 78C285BA | 433-442,713-722 | wall |
| piece-start-transition-west | Start rear half pipe west transition | 5924357D, B2C3B10F | 443-447,681-685 | wall-transition |
| piece-start-transition-east | Start rear half pipe east transition | 5924357D, B2C3B10F | 448-452,686-690 | wall-transition |
| piece-start-rear-deck | Start rear half pipe high deck | 5924357D, B2C3B10F | 453-454,691-692 | deck |
| piece-rollin-east | East entrance curved roll-in | 4AA01023, 25DF6D67 | 455-462,650-657 | roll-in |
| piece-start-flat-east | Start half pipe east flat platform | 4AA01023, 25DF6D67 | 463-464,658-659 | flat |
| piece-start-enclosure-east | Start east side wall panels | 9A7E8FF4, FD06E2F9 | 465-489,624-648 | wall |
| piece-start-enclosure-west | Start west side wall panels | ED79BF62, 365A315C | 607-618,660-671 | wall |
| piece-start-lamp | Start rear overhead lamp | 895344B7, 12633D46 | 619-623,676-680 | support |
| piece-start-flat-west | Start half pipe west flat platform | EE83BEC2, 3DA720B5 | 694-697,773-776 | flat |
| piece-rollin-west | West entrance curved roll-in | EE83BEC2, 3DA720B5 | 698-705,765-772 | roll-in |
| piece-start-window-west | Start west individual window | 4F87E3FF | 761-762 | window |
| piece-start-window-east | Start east individual window | D68EB245 | 763-764 | window |
| piece-start-roof-east | Start east upper roof panel | 1ACB7E91 | 777-784 | roof |
| piece-start-roof-west | Start west upper roof panel | 83C22F2B | 840-847 | roof |
| piece-start-ceiling-middle | Start central flat ceiling panel | F4C51FBD | 848-854 | roof |
| piece-start-ceiling-east | Start east flat ceiling panel | 6AA18A1E | 855-860 | roof |
| piece-start-side-wall | Start roll-in lower west solid wall | AF25296F | 989-997 | wall |
The two start windows are intact `F2184063` sections but collision-open at their starting location. The two frames still block. The source has neither remake door splinters nor a separate animated roll-up-door asset to expose.
### Arena floor, ledges, walls and structural supports
[Section titled “Arena floor, ledges, walls and structural supports”](#arena-floor-ledges-walls-and-structural-supports)
These are named spatial panels, not one uneditable shell. Wall panels include their textured brick/window bands; those painted bands are not separate glass objects. Deep skirts and subterranean details remain individually accounted for even though the kill floor makes them unreachable.
| ID | Element | Groups | Faces | Module |
| --------------------------- | ----------------------------------------------- | -------- | ------------------- | --------------- |
| piece-west-ledge-skirt | West ledge lower skirt | A8556103 | 235-236 | ledge |
| piece-west-foundation-north | West wall north foundation skirt | DA61E06B | 237-238 | wall |
| piece-south-under-ramp | South quarter pipe below-floor transition strip | 1C7398A9 | 239-250 | wall-transition |
| piece-west-foundation-end | West wall foundation end panel | 41C4AC04 | 251 | wall |
| piece-west-pillar-lower | West pillar below-floor section | 57B36E8A | 252-263 | support |
| piece-wall-northwest-bay | North west arena wall bay | 7470EED8 | 298-311 | wall |
| piece-wall-southeast-bay | South east arena wall bay | 731D2AC1 | 324-329 | wall |
| piece-wall-west-north | West arena wall north panel | 0377DE4E | 340-349 | wall |
| piece-floor-west-middle | West floor panel with surface decals | AD183D24 | 350-360 | floor |
| piece-floor-taxi-south | Taxi south floor panel | D4C48580 | 522-529 | floor |
| piece-floor-kickers | Kicker bay floor panel | 4DCDD43A | 530-533 | floor |
| piece-floor-northwest | North west floor panel | 3ACAE4AC | 534-535 | floor |
| piece-floor-rollin-toe | Entrance roll-in toe floor panel | D3A94199 | 536-538 | floor |
| piece-wall-pocket-back | South pocket rear wall panel | 041A1A57 | 539-547 | wall |
| piece-wall-east-entrance | East wall entrance end panel | 93C8C3DF | 861-869 | wall |
| piece-wall-east-north | East wall north of main half pipe | E4CFF349 | 870-875 | wall |
| piece-wall-east-pipe | East wall behind main half pipe and box | 84087AAC | 876-900 | wall |
| piece-wall-east-deck | East wall behind ledge and rail | F30F4A3A | 901-913 | wall |
| piece-floor-pipe-south | Floor south of main half pipe | CDDFB4C1 | 914-918 | floor |
| piece-wall-south-east | South wall east panel | 6A061B80 | 919-924 | wall |
| piece-wall-south-middle | South wall middle panel | 1D012B16 | 925-930 | wall |
| piece-wall-south-west | South wall west straight panel | 8365BEB5 | 931-936 | wall |
| piece-wall-south-end | South west corner wall panel | F4628E23 | 937-949 | wall |
| piece-wall-west-south | West wall south panel | 6D6BDF99 | 950-963 | wall |
| piece-wall-north-tier-side | North tier west side wall | 1A6CEF0F | 964-974 | wall |
| piece-wall-north-tier-end | North tier west rear wall | 8AD3F29E | 975-982 | wall |
| piece-wall-north-tier-back | North tier upper back wall | FDD4C208 | 983-988 | wall |
| piece-floor-rail-drain | Big rail platform and drain floor | 23D1D5ED | 998-1005 | floor |
| piece-floor-rail-south | Big rail south floor panel | BDB5404E | 1006-1018 | floor |
| piece-floor-rail-north | Big rail north floor panel | CAB270D8 | 1019-1023 | floor |
| piece-floor-west-north | West ledge north floor panel | 53BB2162 | 1024-1028 | floor |
| piece-floor-east-north | East entrance floor panel | 24BC11F4 | 1029-1030 | floor |
| piece-south-foundation | South wall deep foundation skirt | A89BBCBC | 1108-1110 | wall |
| piece-west-foundation-south | West wall south foundation skirt | 3192ED06 | 1111-1122 | wall |
| piece-floor-taxi-north | Taxi north floor panel | DA1F0DB2 | 1219-1224 | floor |
| piece-floor-east-drain | East ledge drain floor panel | A4AE710F | 1225-1232 | floor |
| piece-floor-east-south | East south floor panel | 54D6E57B | 1233-1235,1240-1245 | floor |
| piece-east-ramp-end | East quarter pipe ledge-height end cap | 54D6E57B | 1236-1239 | end-cap |
| piece-ledge-west | Shorty’s west ledge top and front | 3999C81C | 1262-1269 | ledge |
| piece-ledge-east | East deck low ledge top and front | A09099A6 | 1270-1284 | ledge |
| piece-crane-hook | Suspended crane hook and chain | E2F52869 | 101-108 | support |
| piece-truss-east-north-01 | East north ceiling diagonal brace | E56DBEAE | 109-111 | support |
| piece-truss-east-north-02 | East north inner ceiling brace | 85AA374B | 112-114 | support |
| piece-west-pillar-upper | West pillar above-floor section | 4B768210 | 143-158 | support |
| piece-truss-south-01 | South ceiling brace west | EBB6369C | 159-161 | support |
| piece-truss-south-02 | South ceiling brace east | 72BF6726 | 162-164 | support |
| piece-truss-west-01 | West ceiling brace north | 82C7F352 | 165-167 | support |
| piece-truss-west-02 | West ceiling brace middle north | F5C0C3C4 | 168-170 | support |
| piece-truss-west-03 | West ceiling brace south | 6CC9927E | 171-173 | support |
| piece-truss-west-04 | West ceiling brace middle south | 1BCEA2E8 | 174-176 | support |
### Rails, taxi, kickers and loose boards
[Section titled “Rails, taxi, kickers and loose boards”](#rails-taxi-kickers-and-loose-boards)
The big rail’s straight and bent bars are separate pieces, including both visible sides. Suppressed post plates get an explicit owner/disposition: they are archival post-remnant pieces, **excluded from the default assembly’s render and collision**, matching `RemoveRailLeftovers`. Do not accidentally restore them, or omit their accounting because they are hidden in the legacy mesh. They can be exposed as optional repair/debris pieces, not structural posts.
| ID | Element | Groups | Faces | Module |
| ------------------------ | ------------------------------------- | -------- | --------------- | --------------- |
| piece-rail-sign | High sign rail rod and wall brackets | 63097BD6 | 25-47 | rail |
| piece-kicker-south-frame | South kicker wooden frame | 51F9A12C | 129-132 | kicker |
| piece-kicker-north-frame | North kicker wooden frame | C8F0F096 | 133-136 | kicker |
| piece-rail-big-bend | Big rail bent bar and end cap | 8D071AFA | 363-365,373-375 | rail |
| piece-rail-big-straight | Big rail straight bar and end cap | 8D071AFA | 366-372,376-382 | rail |
| piece-post-remnant-01 | Big rail suppressed post plate 1 | E67C9BCD | 383 | suppressed-post |
| piece-post-remnant-02 | Big rail suppressed post plate 2 | 0F1F3EF8 | 384 | suppressed-post |
| piece-post-remnant-03 | Big rail suppressed post plate 3 | 96166F42 | 385 | suppressed-post |
| piece-post-remnant-04 | Big rail suppressed post plate 4 | E1115FD4 | 386 | suppressed-post |
| piece-post-remnant-05 | Big rail suppressed post plate 5 | 7F75CA77 | 387 | suppressed-post |
| piece-post-remnant-06 | Big rail suppressed bend post plate | 0872FAE1 | 388 | suppressed-post |
| piece-post-remnant-07 | Big rail suppressed post plate 7 | 917BAB5B | 490 | suppressed-post |
| piece-rail-deck | East deck rail bar | 140E4B40 | 496-512 | rail |
| piece-post-remnant-08 | Deck rail suppressed north post plate | 76C3865C | 513 | suppressed-post |
| piece-post-remnant-09 | Deck rail suppressed south post plate | 01C4B6CA | 514 | suppressed-post |
| piece-kicker-south-board | South kicker riding board | 81140CCD | 517-521 | kicker |
| piece-board-taxi-01 | Taxi north loose board 1 | B79E1F26 | 548-552 | prop |
| piece-board-taxi-02 | Taxi south loose board 2 | 59907E0A | 555-559 | prop |
| piece-board-taxi-03 | Taxi west loose board 3 | B0F3DB3F | 560-564 | prop |
| piece-board-taxi-04 | Taxi north west loose board 4 | 5EFDBA13 | 565-569 | prop |
| piece-board-taxi-05 | Taxi south east loose board 5 | B9459714 | 570-574 | prop |
| piece-board-floor-01 | East floor loose board 1 | D08B6AF6 | 575-578 | prop |
| piece-board-floor-02 | East floor loose board 2 | 39E8CFC3 | 579-582 | prop |
| piece-board-floor-03 | Big rail north loose board 3 | A0E19E79 | 583-586 | prop |
| piece-board-floor-04 | Big rail east loose board 4 | 4759B37E | 587-590 | prop |
| piece-board-floor-05 | Big rail west loose board 5 | 279E3A9B | 591-594 | prop |
| piece-board-floor-06 | Big rail middle loose board 6 | C9905BB7 | 595-598 | prop |
| piece-board-floor-07 | Big rail diagonal loose board 7 | BE976B21 | 599-602 | prop |
| piece-board-floor-08 | Big rail west middle loose board 8 | CEFD9FAE | 603-606 | prop |
| piece-taxi | Taxi funbox | B5300BF6 | 785-839 | prop |
| piece-kicker-north-board | North kicker riding board | 181D5D77 | 1285-1289 | kicker |
| piece-board-taxi-06 | Taxi east loose board 6 | C7F4EBA9 | 1290-1294 | prop |
| piece-board-taxi-07 | Taxi north east loose board 7 | 29FA8A85 | 1295-1299 | prop |
| piece-board-taxi-08 | Taxi south west loose board 8 | CE42A782 | 1300-1304 | prop |
| piece-board-floor-09 | East south loose board 9 | 49823B4C | 1305-1308 | prop |
| piece-board-floor-10 | East middle loose board 10 | A78C5A60 | 1309-1312 | prop |
| piece-board-floor-11 | East north middle loose board 11 | 4EEFFF55 | 1313-1316 | prop |
| piece-board-floor-12 | East diagonal loose board 12 | D7E6AEEF | 1317-1320 | prop |
| piece-board-floor-13 | Big rail south loose board 13 | 305E83E8 | 1321-1324 | prop |
| piece-board-floor-14 | Big rail centre loose board 14 | 50990A0D | 1325-1328 | prop |
| piece-board-floor-15 | Big rail north east loose board 15 | 20F3FE82 | 1329-1332 | prop |
| piece-board-floor-16 | Big rail north west loose board 16 | 57F4CE14 | 1333-1336 | prop |
Taxi bounds are x 121.64–137.36, y 0.83–5.24, z 76.17–81.57. Its eight surrounding sloping boards are separate pieces, not welded into the car. Kicker frames and boards may be grouped for placement but remain independently editable; north board/frame meet near z 44.44–47.60, south near z 61.13–64.28, board tops y 3.30.
### Signs, posters and graffiti overlays
[Section titled “Signs, posters and graffiti overlays”](#signs-posters-and-graffiti-overlays)
These IDs identify source instances for face accounting. The palette uses deduplicated artwork/geometry assets; `pieces.json` maps each instance to its `catalogAsset` and placement transform. Identical artwork need not have an entry for each wall where it appeared.
These are textured overlay faces, not new mesh lettering or remake props. Each row can move independently of its supporting wall. Paired start-area copies stay with the same sign. Materials identify appearance, while the source selector determines ownership.
| ID | Element | Groups | Faces | Module |
| ----------------------------- | ------------------------------ | ------------------ | --------------- | ------ |
| piece-sign-entrance-middle | Middle entrance overhead sign | 912CB677 | 24 | sign |
| piece-poster-northwest-01 | North west upper poster | 7C1384F6 | 95 | sign |
| piece-poster-northwest-02 | North west tall poster | ECAC9967 | 96 | sign |
| piece-graffiti-northwest | North west wall graffiti strip | 9BABA9F1 | 97 | sign |
| piece-poster-northwest-03 | North west lower poster | FB6C2014 | 98 | sign |
| piece-poster-rollin-east | East roll-in wall poster | 82B098B0 | 99 | sign |
| piece-graffiti-east-entrance | East entrance graffiti strip | D04173D7 | 100 | sign |
| piece-sign-south-high | South wall high sign | 7B7E40EF | 137 | sign |
| piece-sign-west | West wall banner | 921DE5DA | 138 | sign |
| piece-sign-northwest | North west wall banner | 0B14B460 | 139 | sign |
| piece-graffiti-east-south | East south wall graffiti | 8C6B1082 | 140 | sign |
| piece-poster-east-deck | East deck wall poster | 626571AE | 141 | sign |
| piece-sign-east | East wall banner | 120F8521 | 142 | sign |
| piece-sign-start-side | Start east side sign | 4948226D, 14E269BE | 271,649 | sign |
| piece-sign-south-underfloor | South foundation banner | 6C8D5CED | 361 | sign |
| piece-sign-start-middle | Start middle window sign | FCDA3430, C6365CE4 | 362,706 | sign |
| piece-sign-start-rear | Start rear wall banner | A7464341, 592AC8B5 | 491-493,672-674 | sign |
| piece-poster-start-rear | Start rear high poster | 3E4F12FB, DFBEBA1B | 494,675 | sign |
| piece-graffiti-start | Start window graffiti | D72CB7CE, 09E75906 | 495,693 | sign |
| piece-sign-skateboarding | SKATEBOARDING hanging sign | A02B8758 | 515-516 | sign |
| piece-poster-foundation-south | West foundation south poster | 4695DD90 | 1123 | sign |
| piece-poster-foundation-north | West foundation north poster | D8F14833 | 1124 | sign |
| piece-poster-box-east | Main pipe east wall poster | 15624138 | 1125 | sign |
| piece-poster-middle-pillar | Middle entrance pillar poster | F5B7A826 | 1126 | sign |
| piece-poster-west-north | West ledge north poster | 3922D6E2 | 1258 | sign |
| piece-poster-west-middle | West ledge middle poster | 4E25E674 | 1259 | sign |
| piece-sign-east-entrance | East entrance wide sign | 92DC24F5 | 1260-1261 | sign |
| piece-graffiti-south-west | South west wall graffiti | E51AD54C | 1337 | sign |
| piece-graffiti-east-north | East north wall graffiti | 6508B5B7 | 1338-1339 | sign |
| piece-graffiti-north-side | North side lower graffiti | 957021C3 | 1340 | sign |
| piece-graffiti-pocket-back | South pocket back graffiti | 8B06D49B | 1341 | sign |
| piece-banner-south-east | South east wall banner | FC01E40D | 1342 | sign |
| piece-banner-south-west | South west wall banner | 0C797079 | 1343 | sign |
| piece-banner-west | West ledge wall banner | DE9AFBE5 | 1344 | sign |
| piece-banner-entrance-middle | Middle entrance wall banner | A99DCB73 | 1345 | sign |
## Ramp module profiles and mating contract
[Section titled “Ramp module profiles and mating contract”](#ramp-module-profiles-and-mating-contract)
Profiles are the original **piecewise-linear** riding surfaces, not circular quarter pipes fitted to a radius. Keep every profile vertex and texture seam. For the table below, `(run,rise)` starts at the toe; run points toward the lip. Width can vary independently only when both mating edges retain the same profile; uniform scaling is the safe initial palette transform. A reflected module needs reversed winding/normals, not merely a negative collider scale.
| Module | Measured profile and height | Width and mating edges |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| main-transition | South profile `(0,0), (4.931,1.334), (8.334,3.364), (10.538,6.671), (11.312,10.054), (11.370,10.692)`; toe y -4.933, lip 5.759 | Six 19.722-wide strips across x 119.781–178.949. Like-side profiles butt together; north is a near mirror with small source differences, not an exact reflection. Toe joins flat at z 19.945 or 32.436; lip joins the outer bank at z 8.595 or 43.806 (no horizontal main-pipe deck). |
| flat | Main bottom y -4.933, depth 12.491 (z 19.945–32.436) | Rectangular bottom joins both main toes. Start flats are separate at y 27.783, not interchangeable at the same elevation. |
| hip-apron | Seven triangular sloping faces from x 100.058 at y 0.5 to the main west profile x 119.781 at y -4.933–0.5 | Joins rectangular flat and the lower parts of both hip transitions. It is not a flat-bottom module despite occupying the bottom of the tapered end. |
| outer-bank / bank-end | Straight bank rises 5.259 over run 7.889, from floor 0.5 to lip 5.759 | Three strips on each side share the main transition lip; opposite toe meets arena floor. Two triangular west end pieces mate the hip end. South east strip is not rectangular: it meets the 3.787-high ledge. |
| hip | West end tapers from lip y 5.759 at x 119.781 to floor y 0.5 at x 100.058; three original faces per side | East edge mates to full main profile; west edge closes onto floor. Diagonal lip ends at z 3.51/48.87, not the straight lip’s z. Keep both asymmetric end types. |
| wall-transition | Typical `(0,0), (1.547,0.522), (4.466,1.972), (6.574,4.447), (7.425,7.077), (8.082,8.063)`; floor 0.5 to lip 8.563 | Straight widths usually 19.722, with longer/shorter source runs retained. Side mates use the full profile, toes meet floor, lips meet deck. Upper north tier has same rise from 8.563 to 16.626; start rear from 27.783 to 35.846. Slight source variations (including steel faces and a 0.019 vertex offset) must not be rounded away. |
| corner | Same wall rise 8.063 swept through the three faceted sectors of a 90-degree concave turn | `piece-pocket-return` and `piece-southeast-corner` mate at both straight profile ends; their split decks fill the inside lip polygon. Rotate to reuse; do not replace with a smooth torus. |
| convex-tip | Same wall rise, each tip half turns 90 degrees in two faceted sectors; full tip turns back 180 degrees | West/east tip halves meet at x 80.335, lip z 85.030; side lips continue at x 77.048/83.622, z 88.317. Mate side transitions at z 88.317. Coping uses two chords across four mesh edges, not four short grind rails. |
| deck | Flat walking surface at the parent lip elevation; room slab thickness 0.329; wall decks generally 3.287 deep | Mate lip boundary without overlap; straight outer edge can mate another deck. Corner decks are polygonal, not bounding-box rectangles. Keep source fascia with its deck. |
| roll-in | Toe `(z,y)` is `(-19.017,0.5)`, then `(-20.564,1.022), (-23.483,2.472), (-32.262,8.447), (-40.499,16.007), (-48.756,22.562), (-58.017,26.952), (-65.752,27.783), (-73.312,27.783)` | Two tapered entrance lanes, separated by the centre pillar/channel. Upper edge meets start flat y 27.783; lower edge meets floor y 0.5. Width changes toward the top: preserve the boundary polyline, not a rectangular substitute. |
| kicker | Board rises from about 0.38 to 3.30 over about 10.06; frame from 0.5 to 3.19 | North/south boards face opposite directions and meet their frame at the original lip. Toe meets floor; side and rear edges are terminal, not quarter-pipe mates. |
| ledge | West/east top y 3.787, floor 0.5; vertical front with shaped plan outline | Top may mate a deck at y 3.787. This is an end/curb module, not a rideable transition. Separate skirt retains its own below-floor faces. |
| end-cap | East quarter pipe terminal face at z 79.770, y 3.710–8.563 | Vertical terminal face joins the 3.787-high east ledge and upper transition/deck boundary. It is not a riding face or a substitute for a full-height wall. |
The below-floor strip at faces 239–250 is an archival transition/end strip (y -7.54–0), not an alternative floor-height wall pipe. Preserve it in the assembly; never use its bounds as the regular wall pipe’s profile.
For joins, compare the **ordered boundary vertices** after placement, not just bounding boxes or nominal heights. Endpoints must coincide at source precision and adjacent riding normals follow the original faceting. The wall ends and hip ends above are the terminal modules; do not invent caps across open riding edges. Splitting additional width strips is permitted later only along existing face boundaries (or an explicitly recorded new cut with interpolated UVs).
## Importer and generator additions
[Section titled “Importer and generator additions”](#importer-and-generator-additions)
No source-face selector is possible for these: their source is the named code/config value below. Every enumerated instance needs an editable transform or setting, not a hidden consequence of the monolithic Warehouse placement. The separate [editable assembly](/docs/thps-warehouse-layout/#editable-construction-assembly) uses these geometry assets, snapshots local grind/support attachments and keeps the game placements and level settings below editable. Ownership does not prevent selecting an individual support or path; see [piece attachments](https://github.com/pythoninthegrass/megabonk_mods/blob/main/docs/asset-bundles.md#piece-attachments).
| Inventory ID / instances | Authoritative source | Editable form and ownership |
| -------------------------------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| added-roof-section-`ordinal` | `ThpsPack.AddRoof`, `WarehouseRoof.Quads`, spill-level ceiling and edge wall profiles | Consolidate emitted faces into larger 40-by-40-world-unit footprint sections, preserving every ceiling step, skirt, underside and inset rather than substituting a flat lid. Shared canonical panel assets are referenced through `pieces.json`; each placed section has its own collision and selection. The room/source roof rows above are separate. |
| added-floor-patch-0 | `FloorPatches[0]`, `AddFloorPatches` | One floor patch, OBJ x 0.52–0.76, z 2.58–3.04, y -0.003 after Y flip; world x 20.876–39.884, z 48.614–85.046, y 0.2624. Keep fitted floor UVs and depth under decals. |
| added-post-0 through added-post-6 | `RailPosts[0:7]`, `AddRailPosts` | Seven independent 0.5-wide box posts below big rail: (x,z) (75,4), (75,16), (75,28), (75,40), (75,52), (78.3,56.8), (81.4,60.5). Bottom y 0.5, top 3.17, five faces each. Associate with straight/bend bars without baking into them. |
| added-post-7 through added-post-9 | `RailPosts[7:10]` | Three deck rail posts at (175.66,49.1), (175.66,61.3), (175.66,73.45), bottom 0.5, top 8.31. Same independent selection and collision as big rail posts. |
| added-grind-mesh-0 through added-grind-mesh-3 | `MESH_RAILS`, `hidden_rail_edit` | Four hidden game rail paths: big straight, big bend, sign rod, deck rail. Attach local path/collider metadata to the matching visible rail; expose path editing/selection even when render-hidden. Transform and delete with the bar; no second visible game rail. |
| added-grind-coping-0 through added-grind-coping-27 | Every ordered `COPING_LIPS` entry via `COPING_RAILS`/`rail_along` | 28 paths: indices 0–3 are main north/south and their west hips, 4–22 are the 19 wall/pocket lips, 23–26 the four west ledge lips, 27 the start lip. Keep exact source endpoints and two-chord pocket tip. A path spanning adjacent strips needs explicit ownership/subpaths without duplicate catch regions; splitting must preserve handoffs and avoid paths shorter than 3.5. Decks must not own a second copy. |
| added-boss-portal | `boss_spawner()`, level `bossSpawner` | Editable boss portal at (170,15.6,27), scale 0.4, yaw 90; retain hidden `BottomPiece` and `Collider`. Select/move the portal independently of the room. |
| added-boss-spawn | level `bossRedirect` | Boolean level setting that keeps the boss redirect on. The redirect picks its own walkable spot, so there is no destination marker; moving room/portal must not silently put the boss inside a closed room. |
| added-radio | `radio_edits`, `RADIO_POSITION` | Ordinary `interactable/boombox` at (90,35.9,-123.5), separately movable/deletable. Playback track belongs to level music setting, not its geometry. |
| added-pile-R1 through added-pile-R5 | Baddy nodes 43,107,109,111,113; `collectibles()` | Five separate `interactable/chest` placements. These are replacements for goal piles, not five extra source crate meshes. Keep final anchored/overridden transforms. |
| added-letter-S, K, A, T, E | Letter nodes 28,29,17,13,12; `LETTER_SHRINES` | Five independently editable `forest/shrines/0..4` objects. T is the merchant prefab. Preserve upright rotation and deliberate overrides, not just node coordinates. |
| added-tape | Node 16 | Separate `interactable/chestFree` inside the room. |
| added-bonus-15, added-bonus-30, added-bonus-31 | `LANE_BONUS_NODES` | Three separately editable free chests. Nodes 148–150 (100/200/500 cluster) are intentionally not placed: no invented kit instances. |
| added-player-start | `PLAYER_START`, level `spawn` | Editable start marker at (109,38,-116), where y is player centre, not feet. Marker saves the level setting independently of the portal. |
| retained-spawn-portal | `PlayerSpawnPortal` | The game still creates its own portal; it is not in the file, so it is not separately selectable. Do not conflate its transform with `spawn`. |
| retained-forest-particles | `ForestParticles` | Created by the game without a placement; not OBJ geometry and not in the file. |
| added-terrain-bootstrap | `authored_grid`, `FLOOR_CELL` | One authored tile (10,7), height -2, slope none, on the 15x15 stock grid. Expose normal tile edits; the game needs it to derive its catch floor. |
| added-clutter-removals | none | A from-scratch level generates no clutter, so there is nothing to remove. |
| settings-gameplay | `killFloorY=-35`, `maxSlopeAngle=88`, `grindRequiresSlide=true`, `noGrass=true` | Authored level controls, editable and saved once per level. Do not bake them into individual ramp assets. |
| settings-music | `music=bundle:thps/04_Superman` | Editable track reference, independent of boombox transform; soundtrack bytes stay baselined. |
| settings-presentation | `title`, `description`, `mapDescription`, `allowedCharacters` | Editable level metadata; initial values come from `build_level()` (Calcium/Tony McZoom restriction). |
| settings-generation | `map=1`, `stageIndex=0`, `tier=0`, `packs` | Editable kit and pack dependencies. `baseline: none`, so nothing is generated from a seed. |
| settings-format | `schemaVersion`, `gameBuildId` | Serialization/compatibility metadata, preserved by save/load rather than represented as scene objects. |
Roof generation must run against the same pre-partition geometry and post repairs as the legacy importer. Re-running a spill calculation separately on each movable piece would change its output and is not equivalent. Store the baked roof panel transforms; do not rebuild it implicitly when a ramp moves.
## Landmark cross-check
[Section titled “Landmark cross-check”](#landmark-cross-check)
This checks the inventory against the structures and M1–M15 map labels in [the reference](/docs/thps-warehouse/#structures). The reference mixes remake imagery with a PS1 extraction: an unmatched remake detail is not evidence for adding geometry to the preserved PS1 assembly.
| Reference landmark / structure | Inventory coverage and limits |
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| M1 winding bank, M10 south notch | South wall transitions, pocket side/tip/return modules and split decks. Their world-space positions, not uncertain map headings, define placement. |
| M2 west platform / ledges | West Shorty’s ledge, its skirt, adjacent floor, deck rail/sign rail where measured; five chest replacements remain separate. The remake/map’s rail names do not prove a different source ownership. |
| M3 taxi | `piece-taxi` plus eight surrounding loose boards; distinct from the wall ramps. |
| M4 half pipe | Six transition strips, rectangular flat bottom, sloping tapered apron, six outer bank strips and their two end pieces, and two hip ends; four authored main/diagonal coping paths. |
| M5 glass corridor / secret room | Three wall frames, arena east wall, five individual window sections, floor and roof slabs, underside support and suspension pieces; tape, boss portal and destination separate. |
| M6 north bank, M7 middle quarter pipe/pillar, M8 east channel | Three lower north runs, upper tier, two independent roll-ins, centre/rear lintel assembly, side panels and window frames. The reference’s exact map-to-world headings remain uncertain. |
| M9 east deck / back pipe | Tier platform, south/east deck/transition rows and the east ledge-height end cap cover the extracted decks and back pipes; do not substitute remake measurements for the source placement. |
| M11 south-east ledge | East low ledge with top/front faces, drain floor and adjacent arena wall. |
| M12 kickers | Two frames and two opposite riding boards, not one combined kicker pair. |
| M13 dark pit | Reference identification is unresolved. Extracted underfloor skirts, below-floor transition and drain panels are all assigned; no fabricated pit prop is needed to close coverage. |
| M14 raised start block / two entrances | High rear transition and deck, start flats, left/right roll-ins, windows, lattice/solid walls and upper roof/ceiling panels. |
| M15 big rail | Straight bar, bent bar, seven added posts, suppressed old plates, floor/drain, loose boards and two hidden grind paths. |
| High rail / SKATEBOARDING sign | Sign rod with original brackets, hanging sign faces and one hidden grind path; no added posts under the wall rod. |
| Deck rail | East bar, three added posts, two suppressed old plates and one hidden grind path. |
| Box piles / letters / tape / bonus icons | Fourteen actual game placements from final generator transforms; extracted goal-pile nodes are not wooden crate geometry. |
| Window wall / brick and graffiti walls | Spatial wall panels, textured window bands and independently assigned sign/poster/graffiti overlays. Source transparency is retained. |
| Ceiling / trusses / lamp / crane | Source upper roof/ceiling panels, ten ceiling/room diagonal braces, chains, crane hook, lamp, pillar sections and added spill roof panels. |
| Remake cones, barricades, truck, door splinters, sunlight shafts | No separate source objects or generator placements identified. Do not invent instances from video-only details; the source partition still covers every face. |
This is a source/measurement audit. The kit’s palette listing, search, thumbnails, placement at world scale, selection, rotation, drag-move, removal and undo/redo were checked in the running editor through the dev harness for a prop, a window and an added piece. Attachment checks cover owner transforms, independent support edits, deletion/undo, isolated copies and saved replay. The three main north transition modules hand off grinds across their split coping paths; the relocated window remains collision-open while its frame stays solid. Visual equivalence and joint collision throughout a complete assembled level remain open. The preservation check guards the shipped Warehouse throughout.
# Thunderstore packaging and releases
The public product is **Megabonk Level Editor**; its package identifier is `Megabonk_Level_Editor`. The repository remains `megabonk_mods`, the assembly is `MegabonkLevels.Mod.dll` and the stable BepInEx GUID is `megabonklevels.runtime`. The team namespace is configured in the root `thunderstore.toml`.
## Package source and build
[Section titled “Package source and build”](#package-source-and-build)
`pkg/` contains the package’s player-facing README, reference manifest, icon and NLayer license. The root README describes the repository, not the package page. The 256×256 icon is a rasterization of `site/public/favicon.svg`; regenerate it with `rsvg-convert -w 256 -h 256 -o pkg/icon.png site/public/favicon.svg`.
From the repository root:
```sh
task package:thunderstore
task package:thunderstore:check
```
The build installs tcli 0.2.4 locally in `.tools/tcli/` if missing, builds the Release DLLs (with Task’s `.env` loading), runs package tests, invokes `tcli build` and validates the resulting `.tools/thunderstore/-Megabonk_Level_Editor-.zip`. It never publishes. After CLI installation and a Release build, `.tools/tcli/tcli build` also builds the ZIP directly.
tcli generates the ZIP’s `manifest.json` from `thunderstore.toml`; `pkg/manifest.json` is a reviewable reference, checked against that generated manifest. tcli adds `namespace` and `FullName` fields; the ZIP checker permits those only with the configured identity. Update both metadata sources and the BepInPlugin display version together for each release.
The ZIP has exactly these files, with no enclosing directory:
```text
manifest.json
README.md
icon.png
BepInEx/plugins/MegabonkLevels/MegabonkLevels.Mod.dll
BepInEx/plugins/MegabonkLevels/NLayer.dll
BepInEx/plugins/MegabonkLevels/NLayer.LICENSE.txt
```
The dependency is `BepInEx-BepInExPack_IL2CPP-6.0.788`. The package does not redistribute BepInEx, its .NET runtime, Harmony, generated game/Unity interop, game assets, PDBs, installers or developer test data. The ZIP checker, `scripts/check_thunderstore_package.py`, rejects unexpected files and DLLs that differ from the current Release output. The older `package:windows`, `package:linux` and `package:macos` tasks describe the legacy MelonLoader installer format and are internal: Task hides them from listings and rejects direct CLI invocation. Do not expose a wrapper that combines them with this BepInEx DLL. Rebuilding legacy packages requires a separately retained legacy checkout, not the public BepInEx source history.
## Shared release version
[Section titled “Shared release version”](#shared-release-version)
`release-please-config.json` defines one root release, with Git tags such as `v0.1.0`. The Python release strategy updates `pyproject.toml`; targeted `extra-files` keep the other product-version fields synchronized:
* the root project entry in `uv.lock`, selected by name rather than array index
* the mod project’s `` and the BepInPlugin display version
* Thunderstore’s `versionNumber` and the reference manifest’s `version_number`
* the site’s `package.json` and both root version fields in `package-lock.json`
The Go installer has no embedded product-version field: its source follows the same Git tag. Do not change the `go` toolchain directive to a product version. Dependencies, game builds, level/pack schemas and the standalone boot spike have their own versions and are not part of this synchronization.
Keep the plugin’s inline `x-release-please-version` annotation; the generic updater uses it to change only that line. The `uv.lock` selector accepts both plain names and the tagged-value objects used by Release Please’s TOML parser, without updating third-party package versions. Two JSON updaters target the npm lockfile’s top-level version and `packages[""].version`; no dependency entries are selected. Version examples in player documentation use placeholders so they do not become stale on the next release.
### First release and subsequent bumps
[Section titled “First release and subsequent bumps”](#first-release-and-subsequent-bumps)
`.release-please-manifest.json` is Release Please’s current/last-released version ledger, not the requested next version. It is bootstrapped at `0.1.0`; the temporary `"release-as": "0.1.0"` override ensures the first release PR targets exactly that version instead of deriving a higher one from existing commits. Remove the override after the first release PR is merged and the GitHub release exists. Otherwise later runs remain pinned at `0.1.0`. The option is supported but deprecated upstream; use it only for this bootstrap, not normal releases.
After that, let conventional commits and Release Please advance the ledger and all version files together. Do not independently release the website or add separate language packages to the manifest unless their release lifecycles are intentionally being split.
`.github/workflows/release-please.yml` runs on pushes to `main`. It reads the repository Actions secret `RELEASE_PLEASE_TOKEN` with contents and pull-request write permissions. The optional placeholder in `.env.example` is for local tooling only: Actions does not read `.env`. Keep the real token out of Git, command arguments and logs. This workflow creates release PRs and GitHub releases; it does **not** publish to Thunderstore, npm, NuGet or PyPI, build a player package, or deploy the website. A push that adds the workflow can start release automation, so review the configuration before pushing.
Check current version consistency with:
```sh
uv run pytest tests/test_release_versions.py -n 0
```
For the optional offline future-bump integration test, install the upstream updaters locally first; this does not contact GitHub or create a release:
```sh
npm install --prefix .tools/release-please --no-save --ignore-scripts release-please@17.11.2
uv run pytest tests/test_release_versions.py -n 0
```
The integration test applies Release Please’s actual updaters and composite update handling to temporary copies at a different version, then checks that all product fields moved and dependency/toolchain versions did not.
### Dependency updates
[Section titled “Dependency updates”](#dependency-updates)
`.github/renovate.json5` groups npm, NuGet, Go modules and Python dependencies. Python uses `pep621` for `pyproject.toml`/`uv.lock` and an explicit `pep723` file pattern for inline script metadata under `scripts/`. There is no Cargo project, so no Rust rule is configured. Other default Renovate managers are not disabled.
The schedule permits new update branches during **Saturday 10–11 AM America/Chicago**, including lockfile maintenance. It is an allowed window, not a guarantee that the bot will run at exactly 10:00. Validate without running dependency updates:
```sh
npx --yes --package renovate@44.222.5 -- renovate-config-validator --strict .github/renovate.json5
```
## Validate a manifest offline
[Section titled “Validate a manifest offline”](#validate-a-manifest-offline)
`scripts/validate_thunderstore_manifest.py` reimplements [the site’s Manifest Validator](https://thunderstore.io/tools/manifest-v1-validator/), which needs a logged-in account and a team you belong to. It ports `ManifestV1Serializer` and the Django 4.2 / DRF validators behind it, including their message text, so a line printed here matches the line the site prints.
```sh
uv run scripts/validate_thunderstore_manifest.py # pkg/manifest.json
uv run scripts/validate_thunderstore_manifest.py --online # plus thunderstore.io
uv run scripts/validate_thunderstore_manifest.py .tools/thunderstore/-.zip
task package:thunderstore:manifest # same, via Task
task package:thunderstore:manifest ONLINE=true
```
It takes a `manifest.json` or a built package ZIP, and the team defaults to `thunderstore.toml`’s `[package].namespace`; `--team` picks another, `--json` emits machine-readable output. Exit 1 means the manifest would be rejected, exit 2 means the input could not be read. `task package:thunderstore` runs it on `pkg/manifest.json` before building, and `task package:thunderstore:check` runs it on the built ZIP.
Offline it checks everything the serializer can decide from the document alone: required fields, the name and version patterns and length caps, a version that is not written in its canonical form, the URL rules, dependency strings, self-dependency, duplicate dependencies, the dependency and installer list caps, installer identifier shape and reuse, and the team name’s shape. The four checks that read the site’s database run only with `--online`, and only once the offline rules pass – as on the site, where they are serializer-level checks that valid fields must reach:
* each dependency resolves to a published package version
* this `namespace-name-version` is not already published
* no package exists with the same name in a different capitalization
* installer identifiers exist, which stays a note because that table is not public
`--online` uses the public read-only JSON API through `scripts/asset_download.py`, so no token is involved: `/api/experimental/package////` and `/api/cyberstorm/listing///` for the team’s package list. The `api/v1/package/...` detail paths return 404 for the same package, so those two are deliberate. An unreachable host is a note, not a failure.
Behaviours worth knowing before debugging a disagreement with the site:
* Extra keys are ignored, which is why tcli’s `namespace` and `FullName` are harmless.
* `PackageReference.parse` takes the version and then the name from the right, so `Dash-Ed-Team-Mythic-TestMod-1.1.0` is one reference and a version with two dots is mandatory.
* A bare `mailto:` address is rejected while `mailto://anything` passes, and `ipfs://` needs something that looks like a host: `ipfs://CID` fails.
## Content and licensing
[Section titled “Content and licensing”](#content-and-licensing)
The Thunderstore package is runtime-only: **no levels, third-party packs or music** are included. In particular, extracted THPS assets and music must not be published with it. Screenshots in the README are publicly hosted links, not bundled game assets. NLayer 1.16.0 is redistributed under MIT with its upstream license notice preserved in `pkg/NLayer.LICENSE.txt`.
The repository currently has no project-wide license file. Packaging does not choose a code/content license on the maintainer’s behalf. Confirm the intended release terms and rights to any newly added content before first publication; adding a level or pack is a separate licensing and manager-layout review.
## Test the final ZIP, not the build folder
[Section titled “Test the final ZIP, not the build folder”](#test-the-final-zip-not-the-build-folder)
1. Quit the game. Back up the manager’s settings/profile metadata and inventory the affected game files. Preserve the existing profile and manual development DLLs outside the manager’s sync tree.
2. Create a **new Megabonk profile** with only the declared BepInEx dependency. Do not reuse a development profile or another plugin’s configuration files.
3. Import the built ZIP with the manager’s local/custom mod import, then Apply. Do not copy the plugin DLL manually as a substitute for this test.
4. Check that the installed plugin and NLayer hashes match the ZIP, and that only one copy of `megabonklevels.runtime` can load. The manager may wrap or flatten the package’s plugin subfolder; inspect the actual installed paths.
5. Launch and check `BepInEx/LogOutput.log` for `Loading [Megabonk Level Editor ]`, `MegabonkLevels mod initialized.` and `Chainloader startup complete`. Check errors associated with this plugin, not just the chainloader’s own `ErrorLog.log`.
6. Confirm the console opens and a separately supplied, pack-free level appears on Map Selection and plays. Enter the editor, make and undo an edit, and save a copy. No mod-specific BepInEx config is expected; developer tools require `MEGABONK_DEV=true` in the launch environment.
7. Review `pkg/README.md` using Thunderstore’s [Markdown Preview](https://thunderstore.io/tools/markdown-preview/), including both externally hosted screenshots. Validate the generated manifest with `task package:thunderstore:manifest ONLINE=true`, and confirm it with the [Manifest Validator](https://thunderstore.io/tools/manifest-v1-validator/), which is the final word. The screenshots use the public showcase site, not private-repository raw URLs. Recheck the editor image URL after a website rebuild changes its asset hash.
8. Record the ZIP SHA-256, profile, installed hashes, log evidence and screenshots. Rebuild and repeat the import test if any packaged file changes afterward.
On the tested macOS setup, r2modmac 1.1.0 manages files but does not successfully launch the Windows game through plain Wine. Use the existing Wine launch path. Its Apply omits the loader pack’s `dotnet/`, `.doorstop_version` and `changelog.txt`; supply these from the dependency ZIP, not from this mod package. Set the prefix’s `winhttp=native,builtin` override (the repo launcher passes it in the environment). Park the MelonLoader `version.dll`; both proxies must not be active together. See [Running BepInEx under Wine](https://github.com/pythoninthegrass/megabonk_mods/blob/main/docs/development.md#running-bepinex-under-wine).
For an isolated test game outside the Wine prefix, inspect the manager-written `doorstop_config.ini` before launching. r2modmac may write a host path such as `\Users\...` without a Windows drive letter. Back up that file, then use paths relative to the game folder instead:
```ini
[General]
target_assembly=BepInEx\core\BepInEx.Unity.IL2CPP.dll
[UnityMono]
dll_search_path_override=BepInEx\core
```
Keep the existing sections and other settings, including the relative `dotnet\coreclr.dll` runtime path. Re-Apply may overwrite these changes. Launch with the game folder as the working directory and the **test copy’s executable** explicitly; the repo’s current plain-Wine launcher still names the default Steam executable even when `MEGABONK_GAME_DIR` changes its working/log directory. Use an explicit `Z:\...\Megabonk.exe` path for a host-side test copy, and point `MEGABONK_GAME_DIR` at that same folder for harness commands. Do not mistake a successful launch of the development install for a manager-install test.
The manager’s sync is bidirectional: deleting installed mod files can also remove them from its profile at the next sync. Restore backups with the manager stopped; do not delete an installed tree to simulate disabling a mod.
With r2modmac 1.1.0, local ZIP imports appear as `Local-Megabonk_Level_Editor-`. Apply preserves the plugin’s nested folder: `BepInEx/plugins/Local-Megabonk_Level_Editor-/MegabonkLevels/` contains the DLL, NLayer and its license. That layout loads correctly; do not flatten it by hand. A clean-profile install was verified under Wine 11.18 with the omitted runtime supplied from the dependency ZIP and relative Doorstop paths. Console, pack-free level loading/gameplay, terrain editing, undo and save-copy all work.
## Release gate and publishing
[Section titled “Release gate and publishing”](#release-gate-and-publishing)
Before the first publish:
* Use the BepInEx runtime on main. Legacy MelonLoader rollback source lives only in the maintainer’s private history backup; it is not a public download or tag. Preserve local loader backups when testing a cutover or rollback.
* Confirm the team’s service account has package-upload permission. Read [Global Rules](https://wiki.thunderstore.io/moderation/global-rules) and any Megabonk-specific rules. Do not distribute game assemblies or unlicensed assets.
* Confirm all linked documentation and feedback destinations are publicly accessible. A private GitHub repository’s source/docs/issues links do not work for players without access; make them public or replace them before release.
* Complete the clean-profile ZIP test, manifest validation and README preview. Record rights/licensing decisions and the exact tested archive hash.
* Confirm community `megabonk`, categories `mods` and `tools`, and NSFW **false** for every upload. The category API currently exposes no AI Generated category; check again before uploading and apply it if it becomes available and applies.
* Obtain explicit publication approval. Building a package is not approval to upload it or to merge the cutover.
Create a **team service account**, not a personal token, at Thunderstore’s team settings. Supply its token only through `TCLI_AUTH_TOKEN` in the shell environment or a gitignored local environment file. Never put it in TOML, command arguments, task notes or logs. After approval, validate and publish the already tested ZIP:
```sh
task package:thunderstore:check
.tools/tcli/tcli publish --file .tools/thunderstore/-Megabonk_Level_Editor-.zip
```
Do not use bare `tcli publish`: it rebuilds by default and could upload a ZIP that was not tested. Recheck the stored SHA-256 immediately before the upload. After publishing, verify the page’s title, icon, screenshots, dependency, category and NSFW settings, then install the published version in another clean profile. That last download check cannot be performed before publication.
Package versions are immutable. Updates require a higher version with the same team/name identity. Retain superseded releases; after a soak period and approval, **deprecate, never delete** an obsolete package. Do not automatically deprecate anything during the initial upload.
## Rollback
[Section titled “Rollback”](#rollback)
Quit the game and disable the package in the manager; Apply and verify the plugin is no longer in the active plugin tree. Keep user-authored `UserData/` files. For a loader rollback, park `winhttp.dll` and `doorstop_config.ini`, restore the preserved MelonLoader `version.dll`, and deploy the DLL from the final legacy tag, not this BepInEx package. Leave inactive loader trees intact. Reverse the proxy renames to return to BepInEx. Restore the original manager profile/settings and manual development artifacts from the pre-test inventory if required.
# Megabonk Level Editor wiki
> Install the mod, play custom levels and explore level-building references.
Guides for the unofficial Megabonk Level Editor. Start with installation, then try a custom level or explore how the Warehouse is built.
The public Thunderstore release has not been published yet. The current mod uses **BepInEx 6 IL2CPP** and the **Windows build** of Megabonk, including through Proton or Wine.
## Start playing
[Section titled “Start playing”](#start-playing)
* [Player guide](/docs/player-guide/) — install the mod, add levels and share them.
* [Bramblecrown Hill](/docs/starter-level/) — an original starter level and a first-edit tutorial.
* [Music](/docs/music/) — custom tracks and level music.
## Explore the Warehouse
[Section titled “Explore the Warehouse”](#explore-the-warehouse)
* [Warehouse reference](/docs/thps-warehouse/) — the source map and its structures.
* [Warehouse layout](/docs/thps-warehouse-layout/) — how the level is assembled in Megabonk.
* [Construction pieces](/docs/thps-warehouse-pieces/) — the editable kit and its mating rules.
## Packages and compatibility
[Section titled “Packages and compatibility”](#packages-and-compatibility)
* [Thunderstore packaging](/docs/thunderstore/) — package contents, releases and loader rollback.
* [Legacy player installer](/docs/player-installer/) — the older MelonLoader format, **not compatible with the current BepInEx DLL**.
## Take the docs with you
[Section titled “Take the docs with you”](#take-the-docs-with-you)
Every page has **Copy page** and **View as Markdown** controls. Use [llms.txt](/llms.txt) for the index or [llms-full.txt](/llms-full.txt) for all published guides.
Technical references outside this wiki link to the [source repository](https://github.com/pythoninthegrass/megabonk_mods/tree/main/docs). Return to the [showcase](https://megabonkmod.party/) to see the editor, console and MCP tools.