diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 2aa42ec..67f978a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,70 @@ +0.1.8.110021 - Horizontal rails (unreleased) + +- Joined rails through posts to matching corners and other timbers, closing the gaps + between window framing and the corner posts. +- Stopped corner pieces adding horizontal returns on an unconnected side. +- Trimmed the backs of corner rails to the inside post so their cut ends do not show + through the adjoining wall. +- Added close-up exterior and interior views to the new Horizontal Rails Preview. + + +0.1.7.110021 - Timber joins (unreleased) + +- Fixed short rail ends sticking out beyond the posts or studs they should meet. +- Made automatic rails finish on diagonal braces at the right height, including thin + walls and short pieces. Continuous rail runs keep their full width. +- Filled the small missing corners where diagonal beams cross between blocks, so + the timber keeps its width through the joint. The joins follow the neighbouring wood. +- Corrected the diagonal bracing on half-step upper floors in the preview street. + The new Timber Joins Preview keeps the earlier saves available for comparison. + + +0.1.6.110021 - Leaded windows (unreleased) + +- Added openable leaded windows to the Late Medieval & Tudor palette, with diamond + or rectangular leading, all six woods, and raw or limewashed surrounds. +- Added full and half-depth frames that fit the existing walls and jettied upper floors. + Stack matching sections for taller casements, or place columns side by side. +- Casements swing outwards. Right-click the frame or open pane to operate a column; + redstone works too. Neighbouring columns can be opened separately. +- Windows check the whole column before opening, so an obstruction leaves it closed. +- Craft leaded panes from glass panes and iron, then add one to a matching post panel. + Shape the panels before glazing. Leading swaps one-for-one, and raw surrounds can + be limewashed singly or in batches. + + +0.1.5.110021 - Crafting (unreleased) + +- Made plain wattle and daub the starting material for timber wall panels. +- Gave full, thin and short walls the same pattern recipes. Arrange three post panels + in a row, column, diagonal or L to make rails, studs, braces or corner/end panels. +- Made paired designs swap one-for-one without extra sticks. Patterned wall panels + can be reshaped back into post panels. +- Added the missing ways to rejoin thin walls and recover shaped corner pieces. +- Added single and batch limewashing for every raw cob and daub shape, including stairs. +- Added crafting hints to the building pieces and rewrote the crafting guide around the new chains. +- Made brackets from matching timber floors and made roof ridges reversible. +- Increased the cob stair recipe to eight stairs and added recovery recipes so shaping + does not waste material. + + +0.1.4.110021 - Jetty brackets (unreleased) + +- Fixed the diagonal supports beneath jetties looking hollow and flickering where + they meet the beams. + + +0.1.3.110021 - Walls and era palettes (unreleased) + +- Tidied up overlapping timber faces to stop flickering at posts, rails and corners. +- Closed the gap above lower walls beneath jettied floors with a matching timber beam. + The open space beneath the projecting part of the floor stays clear. +- Grouped the building kit into Iron Age and Late Medieval & Tudor creative tabs. + Cob, plain daub and thatch appear in both palettes. +- Added a survival recipe setting for choosing one or more eras. Shared materials + stay craftable when an enabled era uses them. Everything is enabled by default. + + 0.1.2.110021 - First alpha (unreleased) This is the first building kit for Iron Age Architecture, starting on Minecraft 1.10.2. diff --git a/README.md b/README.md index 8a9dee9..95a95fa 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,11 @@ to a richly framed townhouse. Matching timbers join around corners and through i All six vanilla wood types are available. Cob comes in blocks, slabs and stairs, and smooth thatch and slate roofs have automatic corners and separate ridge caps. +Leaded windows bring some light into the houses. Choose diamond or rectangular leading, +match the timber and daub to your walls, then stack sections to make taller casements. +They swing outwards with a right-click or redstone, and each column opens separately. +Full and half-depth surrounds fit both ordinary walls and projecting upper floors. + ## Overhanging upper floors Projecting upper storeys, known as jetties, are a central part of the kit. Half-depth walls, @@ -33,6 +38,34 @@ floor edges, and matching corner pieces let the overhang continue around the sid The first alpha targets **Minecraft 1.10.2 with Forge**. Everything is crafted from vanilla ingredients, and the main content families can be enabled or disabled in the configuration. +Start with plain wattle and daub, surround it with matching planks to make post panels, +then arrange those panels into rails, studs, braces and corners. The same patterns work +for full walls, thin walls and short pieces. Paired designs swap one-for-one, and offcuts +can be joined back together. Item tooltips give a reminder while you build. + +You can limewash pieces before or after shaping, singly or in batches of eight. The +[crafting guide](docs/BUILDING-GUIDE.md#crafting) covers the wall patterns, floors, +brackets and roofs. + +## Building through the ages + +The series covers a broad span of traditional buildings. Creative tabs group the pieces by era: +**Iron Age** starts with shared cob, plain daub and thatch, while **Late Medieval & Tudor** +contains the timber-framed houses, jetties and the rest of the current village kit. +Basic materials appear in both palettes. + +The framed palette takes its name from the late medieval and Tudor buildings that inspired it. +Mermaid Street grew over several centuries: Historic England dates the +[Mermaid Hotel's timber frame](https://historicengland.org.uk/listing/the-list/list-entry/1251961) +to the late 15th century, and records a 15th-century front range and 16th-century wing at +[5–6 Mermaid Street](https://historicengland.org.uk/listing/the-list/list-entry/1251938). +These are building palettes for Minecraft rather than exact historical reconstructions. + +For a survival world set in a particular period, choose one or more eras in +`config/ironagearchitecture.cfg`. Shared recipes stay available when any selected era uses +them. All eras are enabled by default. See the [configuration guide](docs/BUILDING-GUIDE.md#era-palettes) +for the settings. + Iron Age Architecture works independently of Iron Age Furniture. You can use the two together to build and furnish your village. diff --git a/docs/BRACKET-FIX-CANDIDATE.md b/docs/BRACKET-FIX-CANDIDATE.md new file mode 100644 index 0000000..774fa59 --- /dev/null +++ b/docs/BRACKET-FIX-CANDIDATE.md @@ -0,0 +1,54 @@ +# Jetty brackets — 0.1.4.110021 + +This records the local checks when the candidate was prepared. References to uncommitted +work and hosted checks describe that point; Git history records later commits and merges. + +The brackets beneath jettied floors now have outward-facing sides and end caps. +Their previous outline ran clockwise, which turned the extrusion inside out and +showed surfaces behind the support. It could also expose coincident faces at the foot. +The corrected outline has the same shape and removes its duplicate closing point. + +The renderer also discards clipped fragments that have no area after conversion to +Minecraft's float vertex coordinates. These fragments were found at the attachment +while checking the actual packaged bracket meshes. Block IDs, metadata, recipes, +collision footprints and bracket drops are unchanged from the era-palette candidate. + +## Review + +Launch the ordinary Eclipse client and open **Jetty Brackets Preview**. Look underneath +the half-block and whole-block projections, especially from either side of a support. +The earlier four preview saves and the frozen 0.1.3 bundle remain separate. +Existing brackets receive the geometry fix automatically when the new version loads. + +## Verification + +- 36 unit tests pass. The added bracket-face test failed against the previous outline + and passes with the corrected one across every valid footprint, reach and direction. +- Build, generated-resource checks, Java 8/package audits and the Eclipse production + classpath check pass. Normal Eclipse output is synchronized without the test mod. +- Packaged core, saved-world reload and the 0.1.2 preview import pass all 4,448 saved + states, 1,077 recipes, placement/drop checks, rotations, mirrors and walking checks. + The core run also repeats the eight era policies and 16 content configuration cases. +- The native client inspects actual baked bracket vertices in every available wood, + reach and saved rotation. Each emitted bracket face must have area and point out + of the solid. The first packaged check caught collapsed attachment fragments; + the renderer correction passes the same assertion. +- Twelve native captures include close views of a full bracket from both sides and + the feet of half- and full-reach brackets. Both era tabs retain their expected contents. +- The new preview is saved and reloaded with Architecture alone, without the test mod. + Automated checks and captured views remain separate from the user's gameplay review. + +Runtime SHA-256: +`135db73299ec934d6040172dfd1901b143c3b4aaf71c7b4d177971162f1e386d` + +Checked runtime evidence: `D:\IronAgeArchitecture\manual-testing\brackets-0.1.4-checked`. +The failed unit/runtime evidence remains under `brackets-0.1.4-early` and +`brackets-0.1.4-final`. The latter also holds an initial passing native run before the +closeup cameras were lowered beneath the floor. No failed result was relabelled. + +The separate candidate is `D:\IronAgeArchitecture\candidates\0.1.4.110021-jetty-brackets`. +It includes the runtime, sources and Javadoc artifacts, checksums, screenshots, +receipts, clean preview ZIP, source snapshot and manifests. + +The changes remain local and uncommitted on `feature/1.10-era-palettes-and-joins`. +Nothing has been pushed, merged or published for this increment. Hosted CI has not run. diff --git a/docs/BUILDING-GUIDE.md b/docs/BUILDING-GUIDE.md index f0ee9bb..36563eb 100644 --- a/docs/BUILDING-GUIDE.md +++ b/docs/BUILDING-GUIDE.md @@ -3,10 +3,10 @@ Permanent building components for the Iron Age series, starting on Minecraft 1.10.2 Forge. Architecture runs independently of Iron Age Furniture. -The current local candidate, 0.1.2.110021, contains raw and limewashed cob blocks, +Version 0.1.8.110021 contains raw and limewashed cob blocks, stairs and slabs; modular wattle-and-daub walls in all six vanilla woods; and smooth thatch/slate roofs with automatic corners and separate ridge caps, plus jettied-storey walls, -timber floors and support brackets. There are 460 visible +timber floors, support brackets and openable leaded windows. There are 510 visible variants including the wood and finish choices. Architecture remains standalone. ## Wattle and daub @@ -17,6 +17,16 @@ Place posts at your chosen bay spacing and rails at floor/head heights. Matching joins automatically at intersections and around corners. Studs extend into rails and diagonal runs terminate at matching posts, corners or rails. Wood grain follows each member. +A rail from a window or sill/head panel joins through intervening posts to a matching +corner or other timber. Unsupported ends finish on their post, stud or brace. Corner +pieces draw rails on their connected sides. Use sill/head panels for a deliberate +continuous rail. Meeting braces also carry their width across the +adjoining panel corners, using the same wood. Plain infill does not add complete rails. + +Check the direction of a diagonal when changing the occupied half of a wall. Facing a +panel the other way reverses its slope in the building. Craft a brace alone to swap its +rising/falling pattern when needed, then continue the line through the next section. + All modules are ordinary wall blocks with raw or limewashed infill. The connections are derived from neighbouring blocks, so there are no wall tile entities or ticking components. The original timber_frame block IDs and four item subtypes remain readable. @@ -34,6 +44,11 @@ quarter and L-shaped floors provide the matching footprints. Joists follow the d you face; matching neighbouring floors join their boards and perimeter beams automatically. Click the empty corner when placing an L-shaped floor. +Where a floor sits directly above a wall, a matching timber wall-head beam closes the +space up to the floorboards. It follows the lower wall's footprint, including half-depth +walls and corners, so the cantilevered part remains open underneath. It updates when the +lower wall changes; no extra piece needs placing. + A half-block facade step uses the other occupied half of a cell, or the next half in the neighbouring cell. Repeat this on successive floors for 0.5 + 0.5, 0.5 + 1, or larger projections. The added floor area is usable inside. Continue a wall run to inherit its @@ -50,34 +65,144 @@ faces that direction. Adjacent matching slopes form hips and valleys using stair Ridges run along the direction the player looks; place them one row above the high ends of the last slopes. Their half-block apex completes the 45-degree roof profile. +## Leaded windows + +Use full-depth surrounds in full walls and half-depth surrounds in thin walls. Both come +in all six woods, with raw or limewashed daub and diamond or rectangular leading. +Each section is one block wide and high. Matching sections stacked above one another +form one tall casement, with a continuous opening. Side-by-side columns share an upright +mullion but operate separately. + +Click the left or right half when placing the first section to choose its hinge. The +window faces towards you and opens outwards. Thin windows follow the occupied half of +the adjoining thin wall; stacked windows inherit their column's facing and hinge. +Sneak while placing to choose the alignment yourself. Holding a window item lets you +add more sections without opening the one you clicked. + +Right-click either the frame or its open pane to open or close the whole matching column. +Keep the outside cell of every section empty and leave room for the swing, including +entities. If anything blocks it, the column stays as it was. Close separate columns +before filling a gap between them. A clear two-block-tall open casement is wide enough +for a normal player to pass through; step onto the sill first. + +Powering any section operates its column. A manual change stays in effect until the +power next changes, just like a wooden door. A powered window blocked from opening +tries again when the obstruction is removed. Windows open instantly through 90 degrees. +Breaking a section, including through its open pane, returns its intact window item. + ## Crafting +Start with a few base materials, then shape them into the pieces you need. Match the wood, +finish and pattern within each recipe. Shaping and recombining keep the amount of material; +changing a timber pattern does not need extra sticks. + +### Cob and plain wattle and daub + - Four dirt, four clay balls and one wheat, shapeless: eight cob. -- Cob plus bone meal: one limewashed cob. -- Three cob in a row: six slabs. The stair pattern: four stairs. -- One cob, shapeless: one wattle-and-daub infill block of the matching finish. -- Four matching planks around cob: four post-and-daub blocks of that wood and finish. -- A post block plus two sticks: a cross-braced block; add one stick for a single rising brace. -- Two post blocks in a row: two sill rails. Craft a sill/head rail alone to swap its pattern. -- Craft a post block alone for a narrow stud; add a stick to that stud for close studding. -- Three post blocks in a column: three corner posts. Craft corner/end posts alone to swap. -- Craft a rising/falling brace alone to swap its direction. Raw daub/modules accept bone meal. -- Two matching full walls vertically: four thin walls. Two thin walls vertically: four short walls. -- Two short walls horizontally: one thin wall. Craft paired thin/short patterns alone to swap. -- One short end post: quarter corner; two vertically: half corner; three in an L: L corner. -- Craft corner posts alone to switch their outside/inside pattern. Raw partial walls accept bone meal. -- Three matching planks above three sticks: six timber jetty floors. Two full floors vertically: - four halves; two halves vertically: four quarters. Two cuts horizontally recombine into one larger piece. +- One cob alone: one plain wattle-and-daub block. Craft the infill alone to turn it back into cob. +- Three cob in a row: six slabs. Two slabs side by side: one cob. +- Six cob in the usual stair pattern: eight stairs. Four stairs in a square: three cob. + +### Timber wall patterns + +Put plain wattle and daub in the centre, with one matching plank above, below, left and +right, to make four post panels. Choose the infill's finish before crafting, or limewash later. + +Shape three matching post panels into the family you want: + +| Arrangement | Result | +| --- | --- | +| Three in a horizontal row | Three sill rails | +| Three in a vertical column | Three narrow studs | +| Three along a diagonal across the crafting table | Three rising braces | +| Three in an L | Three corner/end wall panels | + +These patterns work for full walls, thin walls and short pieces. Start with the single-post +pattern; a cross-braced post panel can be swapped back by crafting it alone. + +Craft one paired-pattern piece alone to swap its design: post/cross brace, sill/head rail, +single/close studs, rising/falling brace, or corner/end post. Shaped corner pieces swap their +outside/inside pattern in the same way. A row of three matching rails, studs, braces or +corner/end wall panels returns three post panels of the same size, wood and finish. + +### Cutting walls and making returns + +| Input | Arrangement | Result | +| --- | --- | --- | +| Two matching full walls | Vertical | Four thin walls | +| Two matching thin walls | Vertical | Four short walls | +| Two matching short walls | Horizontal | One thin wall | +| Two matching thin walls | Horizontal | One full wall | + +Cut corner/end panels become straight end posts. Rejoining them returns a full end post; +craft it alone if you want the full corner pattern instead. + +For shaped returns, use short end posts: one makes a quarter corner, two vertically make +a half corner, and three in an L make an L-shaped corner. Two matching shaped corner pieces +side by side recover their short end posts: two from quarter corners, four from half corners, +or six from L-shaped corners. Either inside/outside pattern can be recovered this way. + +### Leaded glazing + +- Surround one iron ingot with eight glass panes to make eight diamond-leaded panes. +- Craft one leaded pane alone to swap between diamond and rectangular leading. +- Combine one pane with one matching principal-post panel to make one window. The + panel supplies its wood, daub finish and full or half depth. Use the single-post pattern. +- Shape panels **before glazing**. Windows cannot be cut into thin or short walls. +- Craft a window alone to swap its leading while keeping the same surround and depth. +- Limewash a raw window with bone meal, singly or in a batch of eight identical windows. + +### Floors, brackets and roofs + +- Three matching planks above three sticks: six timber floors. +- Cut floors with a vertical pair: two full floors make four halves; two halves make four + quarters. Join cuts with a horizontal pair: two quarters make one half; two halves make one full floor. - Three quarter floors in an L: one L-shaped floor. Craft an L-shaped floor alone to recover three quarters. -- Two matching planks with a stick beside the lower plank: two full-reach brackets. Craft one alone - for two half-reach brackets; craft two halves horizontally to recombine. -- Three wheat above three sticks: six thatch roofs. Replace wheat with stone for slate roofs. -- Two matching roof slopes in a row: three ridge caps. +- One full timber floor plus one stick, shapeless: two full-reach brackets in that wood. + One full-reach bracket alone makes two half-reach brackets; two halves side by side make one full bracket. +- Three wheat above three sticks: six thatch roof slopes. Use stone instead of wheat for slate. +- Two matching slopes vertically: four ridge caps. Two ridge caps side by side: one slope. + +### Limewashing + +Every raw cob, daub or timber-and-daub piece can be limewashed after shaping. Use one piece +with one bone meal for a single conversion, or surround one bone meal with eight identical +pieces to wash the whole batch. Wood, pattern and shape stay the same. Slabs, stairs and +shaped corners follow this rule too. + +## Era palettes + +The creative tabs follow building eras. **Iron Age** currently holds the shared cob, +plain daub and thatch pieces. **Late Medieval & Tudor** holds the full timber-framed +village kit, including leaded windows, jetties, brackets and slate roofs, along with those shared materials. +The Iron Age palette is a starting selection; a dedicated roundhouse kit is still to come. + +Choose which eras can be crafted in survival in `config/ironagearchitecture.cfg`: + +```text +recipes { + S:survivalEras < + late_medieval_tudor + > +} +``` + +Use `iron_age` for the earlier palette, or put both IDs on separate lines to enable +both. The default entry `all` includes every era. An empty list disables all Architecture +recipes. Unknown names are logged and ignored, so a misspelling cannot enable other eras. +Restart the game or server after changing this setting. + +A shared recipe stays available if any enabled era uses it. The setting filters this +mod's crafting recipes; creative tabs and existing placed blocks stay available. Server +owners should set the same recipe selection on their clients. Existing content-family +switches still apply separately. All content families are enabled by default. Framed panels require the cob family. +The `blocks.leadedWindows` switch controls both glazing and windows and requires timber +frames and cob. Era recipe filtering is separate from this content switch. Thatch and timber-bearing walls burn; plain daub, cob and slate do not. There is no world generation or collapse simulation. See [development instructions](DEVELOPMENT.md) for builds, Eclipse and disposable runtime checks. This is an early alpha. Automated checks and manual visual/gameplay review are recorded separately. -The current test world is Jettied Street Preview; both earlier preview saves are retained separately. -See [the jetty candidate handoff](JETTIED-STOREYS-CANDIDATE.md) for the preview and verification record. +The current test world is Horizontal Rails Preview; earlier preview saves are retained separately. +See [the current candidate handoff](HORIZONTAL-RAILS-CANDIDATE.md) for the verification record. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index c16fb8c..7a204a7 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -39,6 +39,29 @@ Slopes rise one block per block; ridge caps rise half a block and belong one row The roof mesh is smooth; collision is a conservative approximation within 1/16 of the surface. No architectural block needs a tile entity or scheduled ticking. +WindowBlock is a separate light-transmitting family. Metadata is horizontal facing in +bits 0-1, open in bit 2 and cached power in bit 3. Wood, finish, depth and leading live +in identity. Hidden right-hinged and rear-half thin identities have no inventory items; +all drops and picks resolve to the matching canonical window. Existing saved IDs stay intact. +Rear-half placement keeps a thin wall's occupied plane independent of the outward normal. + +WindowGeometry supplies sash-local UVs, clipped owner/helper meshes and collision boxes. +Timber and lead use the solid layer; glazing uses the translucent layer. The open sash is +split at the cell boundary without adding faces at the cut. Each outside cell contains +one hidden WindowLeafBlock pointing back to its owner, so its portion remains rendered, +targetable and solid when the two cells belong to different render chunks. + +WindowController preflights a complete matching vertical column, snapshots changed cells +and commits under a callback guard. Opening needs an empty loaded outside cell per section +and an entity-free swept prism. It never consumes an obstruction or loads a neighbouring +chunk. Cached power changes operate the column; manual overrides leave that cache alone. +A failed powered opening retains the old cache and retries on later neighbour updates. +Helpers forward activation/mining, have no independent drops and remove only owned links. +One-off chunk-load tasks reconcile loaded borders, including deferred power queries; +there are no tile entities or scheduled block ticks. A short-lived queue retries only +entity-blocked power edges every ten world ticks, because entity movement does not send +block updates. Idle windows and ownership links are never polled. + PartialWallBlock adds fixed thin/short/corner footprints without changing existing saved IDs. WallRuns compares world-space occupied strips, including offset planes and shortened endpoints. Partial walls retain the original pattern/finish/facing layout; short end posts use two finish @@ -48,24 +71,105 @@ Partial walls use slab-style neighbour brightness and are not classified as full JettyFloorBlock uses facing bits 0-1, joist-axis bit 2 and bracket-reach bit 3. Shape and optional local bracket direction are carried by block identity. Bracketed floor IDs have no ItemBlocks; their drops contain the canonical floor and the matching bracket item. The floor's fixed -footprint never changes with neighbours. Only covered perimeter faces are derived at render time. +deck footprint never changes with neighbours. Covered perimeter faces and a lower wall-head beam +are derived from neighbouring coverage. Connection bits 16-19 describe the quarters occupied by +the supporting wall. The beam fills only those quarters below the upper-half deck; cantilever air +remains open. Removing or replacing the lower wall invalidates the floor mesh. JettyFloorGeometry supplies boards, timbers, corner ties and brackets to the same collision/model path as walls. Board edge UVs include vertical depth and stay inside their atlas sprite bounds. FrameItem checks collision against the chosen placement state, including hidden handedness, rather than the canonical inventory block's default shape. +SurfaceGeometry clips buried faces from the shared solid parts. At coplanar intersections, +boards, posts and rails have deterministic ownership; braces sit slightly behind the main frame. +The collision union continues to use the solid parts. Horizontal ties reverse their reflected +mesh winding so the exposed underside remains visible. +One-sided automatic rails are clipped against the timber outlines at the rail's height. +Partial walls do this after shifting short-piece posts and clipping their footprints. +Explicit rail panels and runs requested from both sides retain their full span. The +preview generator converts its world-space brace slope to the selected strip's local +axis; rear-half strips reverse local X. Native checks verify eight complete brace bays. +TimberCorners fills the two small missing quadrants of a diagonal block-corner joint +inside the adjacent wall cells. Two matching, aligned brace endpoints must agree on +wood; lone endpoints, mixed woods and offset planes do not produce a continuation. +A separate unlisted property carries four three-bit timber choices. Part.timber selects +the matching existing sprite on otherwise plain infill; the model cache includes this +property. Shared parts supply rendering, collision and targeting. No saved metadata, +block IDs, helper blocks or tile entities are needed for these junctions. +RailJoins completes source-to-anchor runs through structural panels. It starts from +the direct rail requests and searches the opposite side for aligned matching timber +at that height. It never recursively evaluates another block's connections. Partial +corners draw only their requested outgoing legs. Native verification measures positive +timber contact at 1,920 shared faces across 960 corner/post/source arrangements and +checks that removing the source clears the derived rails. +The back face of each corner rail starts at the inside post. Carrying it through the +full corner cell exposes its cut end on the perpendicular wall as a detached wood mark. +Bracket outlines must run counter-clockwise without a repeated closing vertex. Clockwise +outlines turn the entire extrusion inside out, exposing the back of the attachment surface +and making the support appear hollow. Unit tests and the native client check the actual +bracket face winding; the native check covers both reaches and every saved direction. + +## Era membership + +EraCatalog supplies both creative membership and survival recipe eligibility. Basic cob, +plain daub and thatch are shared by Iron Age and Late Medieval & Tudor; timber framing, +jetties, brackets and slate belong to the latter. Add future palettes to Era and assign membership +in the catalog. Shared items expose every relevant creative tab through their Item implementations. + +ArchitectureConfig parses recipes.survivalEras at startup. All recipe registration passes through +one output-membership check; block/item registration is independent of this selection. The policy +uses a union of the selected eras, defaults to all and logs unknown IDs without enabling them. +Changing it requires a restart. Client and server recipe selections should match; this legacy +Forge version does not synchronize the config or recipe catalog automatically. + +## Crafting conventions + +ArchitectureRecipes keeps vanilla-material starters separate from shaping. Full infill and +four matching planks start timber walls; post panels then supply every family at the same +size. Three posts in a row, column, diagonal or L produce three rails, studs, braces or +terminal panels. Paired designs flip one-for-one. Rows of three other wall panels recover +base posts. Raw/washed finish and wood metadata stay explicit in every step. + +Two vertical larger pieces cut to four smaller pieces; two horizontal cuts rejoin into one +larger piece. Corner recovery returns the equivalent number of short end posts. Cob stairs +yield eight from six cob and recover three cob from four stairs. Roof slopes/ridges follow +the same 2:4 cutting and 2:1 joining convention. Brackets start from matching full floors. +Single limewashing and eight-piece batches share one helper and keep shape/pattern metadata. +Leaded panes start with eight glass panes around iron. One pane and one matching full/thin +principal-post panel produce one window. Glazed windows have no cutting/rejoining recipes. +Conservation checks account for frame and glazing independently, preventing glass duplication. +The building guide is the player-facing reference; translated crafting hints live in the +resource generator and item tooltips. + ## Native probes -Run scripts/invoke-runtime-probe.ps1 with core, reload, furniture, disabled, legacy and legacy-daub modes after prepareProbe. -Only Forge/server libraries and the explicitly named Furniture JAR are read from ServerTemplate; +Run scripts/invoke-runtime-probe.ps1 after prepareProbe. Modes cover core, reload, furniture, +disabled, windows-disabled, legacy, legacy-daub, legacy-jetties, iron-age, late-medieval, +mixed-eras, no-eras and invalid-era. +Only Forge/server libraries and the explicitly named Furniture JAR are read from retained runtimes; no existing worlds or configs are copied or changed. Each mode has an isolated runtime except reload, which verifies the saved core world. Servers bind to loopback. Keep failed logs when correcting failures. legacy extracts a copy of the frozen 0.1.0 preview and verifies all 96 original timber state/variant records. legacy-daub extracts the frozen 0.1.1 preview and verifies all 656 saved block/state records. -Use a fresh runtime root for each candidate; current default is manual-testing/jetties-0.1.2. +legacy-jetties extracts the frozen 0.1.2 preview and verifies all 4,448 saved block/state records. +The core run also loads eight era config cases, rebuilds only Architecture recipes, checks allowed +and excluded crafting grids and restores the original global recipe list. Separate startup modes +prove the selection survives pre-init and does not change registered content. Wall-head checks cover +all woods, partial-wall footprints and rotations, plus support removal and vanilla solid walls. +The core run also enables recipeMatrix: it checks every shifted/mirrored shaped grid and two +shapeless arrangements, then audits all material-only conversions and visible-item reachability. +The reachability solver respects wildcard ingredients and repeats under each era selection. +Known starter recipes also reject mixed wood. Receipts bind both matrices to the packaged JAR. +Window checks cover 2,304 metadata states, 576 directional columns, 144 saved columns, +placement/alignment, independent columns, blocked bridges, hand/power transitions, swept +entity clearance, owner/helper rays, player passage, harvesting, explosions and link repair. +Use a fresh runtime root for each candidate; current default is manual-testing/horizontal-rails-0.1.8. The optional client probe requires iaa.probe=true, iaa.clientProbe=true and iaa.keepRunning=true. -It creates its own Jettied Street Preview, checks all mesh/material variants, captures native screenshots, then shuts down. +It creates its own Horizontal Rails Preview, checks all mesh/material variants and both +actual creative-tab contents, captures twenty native screenshots, then shuts down. +Every visible item must also provide a translated crafting hint without tooltip errors. +The client script's PreviewName argument selects a separate save without replacing an earlier preview. After verification copy the preview save into the ordinary run/saves directory and launch without the probe. Screenshots and automated walk/collision checks do not constitute the user's manual acceptance. diff --git a/docs/ERA-PALETTES-CANDIDATE.md b/docs/ERA-PALETTES-CANDIDATE.md new file mode 100644 index 0000000..bfb3c8a --- /dev/null +++ b/docs/ERA-PALETTES-CANDIDATE.md @@ -0,0 +1,70 @@ +# Walls and era palettes — 0.1.3.110021 + +This records the local checks when the candidate was prepared. References to uncommitted +work and hosted checks describe that point; Git history records later commits and merges. + +The changes are local on `feature/1.10-era-palettes-and-joins`, based on the merged +0.1.2 code. This increment has not been committed, pushed, merged or published. + +## Try the changes + +Launch the ordinary Eclipse client and open **Era Palettes and Joins Preview**. +The previous three saves are retained separately. The new preview has been saved and +reloaded with Architecture alone, without the verification mod. + +Look along the timber intersections and beneath the overhanging floors. Buried and +coplanar timber faces now have a single visible owner; braces sit slightly behind posts +and rails. A timber wall-head beam closes the half-block opening between lower walls +and upper-half floors. It follows the wall below, including corner footprints, and +leaves the projecting part open underneath. Existing block IDs and saved metadata stay +the same, so older builds receive the revised geometry automatically. + +The creative tabs are **Iron Age** and **Late Medieval & Tudor**. Cob, plain daub and +thatch appear in both; framing, jetties, brackets and slate are in the latter. The earlier +palette currently contains shared basics rather than a complete Iron Age building kit. +The [building guide](BUILDING-GUIDE.md#era-palettes) explains the survival recipe setting. + +Mermaid Street contains buildings from several periods. The framed palette's name is +based on Historic England's late-15th-century date for the +[Mermaid Hotel](https://historicengland.org.uk/listing/the-list/list-entry/1251961) +and its record of a 15th-century front range and 16th-century wing at +[5–6 Mermaid Street](https://historicengland.org.uk/listing/the-list/list-entry/1251938). +It is a style palette, not a claim that every piece reproduces one historical building. + +## Checks + +- 35 unit tests pass, including a regression that failed before the wall-head beam was added. +- The build, Javadoc, 771 generated resources, Java 8 artifact audit and Eclipse production + classpath checks pass. The normal Eclipse output is synchronized and excludes probes. +- Twelve packaged server runs pass: core/reload, five era selections, three older preview + imports, Furniture coexistence and all content families disabled. +- The initialized Forge era matrix checks eight parsed configurations, allowed crafting + and excluded outputs, shared-material membership and unchanged creative availability. +- Default, late-medieval and mixed selections have 1,077 recipes. Iron Age alone has 19; + empty and unknown-only selections have zero. All keep the 460 visible variants and + 4,448 tested saved states. The existing all-content-disabled switch still yields zero. +- Older previews retain 96 original timber records, 656 wall/roof records and all 4,448 + records from the previous jetty alpha. Drops, rotations, mirrors, item placement and + actual walking/collision checks also pass. +- Native client checks cover every wall/floor/roof model and inventory subtype, material + sprites and the actual creative-tab contents: 14 shared variants and 460 later variants. +- The ordinary preview reload uses only Architecture and Forge's three loader/game mods. + Its second load has no missing verification-mod warning. + +Runtime SHA-256: +`523c91a24825b6f2fc1cb2f47dcf00a2ede0c56fee363759433fdc92a8a4053c` + +Evidence is under `D:\IronAgeArchitecture\manual-testing\eras-joins-0.1.3-final`. +Early failures and intermediate captures are retained under the separate `-early` root, +including the red wall-head test and a probe-only compile correction. The first final +palette capture did not keep the requested tab selected; its files are retained separately +from `native-client-tabs-04`, whose probe selects the private tab setter explicitly and +checks the selected tab before capture. The intermediate capture assertion failure and +stopped capture are retained. These changes affect only the test mod, not the player JAR. +Automated checks and screenshot inspection do not replace the +user's manual visual/gameplay review. Hosted CI has not been run for this local increment. + +The separate bundle is `D:\IronAgeArchitecture\candidates\0.1.3.110021-era-palettes-and-joins`. +It contains the three player-facing artifacts, checksums, source snapshot, clean preview ZIP, +selected screenshots and verification logs/receipts. The earlier bundles' manifests were +rechecked: all 21, 25 and 35 delivery files respectively remain unchanged. diff --git a/docs/HORIZONTAL-RAILS-CANDIDATE.md b/docs/HORIZONTAL-RAILS-CANDIDATE.md new file mode 100644 index 0000000..e7da6b0 --- /dev/null +++ b/docs/HORIZONTAL-RAILS-CANDIDATE.md @@ -0,0 +1,66 @@ +# Horizontal rails — 0.1.8.110021 + +This records the local checks when the candidate was prepared. References to uncommitted +work and hosted checks describe that point; Git history records later commits and merges. + +The remaining horizontal gaps were real. A corner could draw a rail towards a window +while the intervening principal post drew only the half nearest that window. That left +a gap between the corner's rail and the centre of the post. Some corner pieces also +grew a return on their unconnected side. + +Rails now complete a run through intervening structural panels to matching timber on +the other side. The search starts from an existing window or rail request and checks +the occupied wall strip and timber at that height. It stays bounded and does not ask +neighbouring blocks to calculate their connections recursively. Plain infill still +stops a run; unsupported ends retain the previous post/brace trimming. + +Corner pieces draw only their connected legs. The back faces finish at the inside +post instead of continuing through the other wall. This removes the detached little +wood marks exposed by the first exterior close-up. + +The changes use existing blocks, recipes and saved metadata. The content totals stay +at 510 visible variants and 2,010 default recipes, with 36 for Iron Age survival. + +## Verification + +- Two corner-direction tests fail against 0.1.7. The packaged 0.1.7 JAR also fails the + new shared-face contact check with the reported corner/post gap. A third test + reproduces the rear-rail end showing through the adjoining wall. +- All 50 unit tests pass after the fixes. Build/Javadoc, 967 generated resources, + package/Java 8 audits and ordinary Eclipse output/classpath checks pass. +- The new native check covers 960 corner–post–rail/window runs: six woods, both + finishes, four facings, sill/head heights, full corners and all partial corner shapes. + It measures positive timber contact at 1,920 shared faces and removes each source + to check that the derived rails disappear. The windows-disabled mode covers 480 runs. +- Thirteen packaged server modes pass, including reload, Furniture coexistence, + family switches, era restrictions and the three previous-generation world imports. + The window, diagonal-junction, saved-state and crafting matrices remain green. +- The native client checks models, materials, palettes and translated item hints. + Twenty captures include dedicated exterior and interior window-to-corner views. + The bundle also retains the 0.1.7 street screenshot for comparison. +- The installed Horizontal Rails Preview layout is checked through a disposable copy + with the final player JAR, including a plain reload without the probe. The final + rear-face trim changes derived geometry, so the placed preview layout is reused. + All 200 files in the seven earlier preview saves retain their original hashes. + +Runtime SHA-256: +`e04d2d4f8d07efbca18da112e318a27e34ac2cbd94d9fd04afeb0ea0013b27a9` + +Evidence: `D:\IronAgeArchitecture\manual-testing\horizontal-rails-0.1.8-qualified-02`. +The study root retains the red unit and native results. Its initial runtime used the +old probe because the failing unit task stopped probe preparation; that run is not +the new contact check. `red-runtime-02` contains the reproduced gap after preparing +the new probe. The earlier 0.1.8 render and receipts remain in the separate first-pass +roots; they exposed the rear-face cut ends and are not the final candidate proof. + +Candidate: `D:\IronAgeArchitecture\candidates\0.1.8.110021-horizontal-rails`. +It contains player/sources/Javadoc artifacts, checksums, source snapshot, building guide, +changelog, preview ZIP, screenshots and hash-bound receipts/logs. + +Restart the Eclipse client and open **Horizontal Rails Preview**. Check the sill and +head rails from the window jamb, through the next post and into the corner, from both +the street and the room. The screenshots have been inspected; player gameplay review +remains separate from the automated results. + +Earlier saves and frozen candidates remain separate. This increment is local and +uncommitted on `feature/1.10-era-palettes-and-joins`. Nothing was pushed, merged or published. diff --git a/docs/INITIAL-CANDIDATE.md b/docs/INITIAL-CANDIDATE.md index 3ea9699..0b46a62 100644 --- a/docs/INITIAL-CANDIDATE.md +++ b/docs/INITIAL-CANDIDATE.md @@ -1,5 +1,8 @@ # Initial cottage kit — local handoff +This records the local checks when the candidate was prepared. References to uncommitted +work and hosted checks describe that point; Git history records later commits and merges. + Version: **0.1.0.110021**, Minecraft 1.10.2, Forge 12.18.3.2511. The implementation is local and uncommitted on feature/1.10-initial-cottage-kit. diff --git a/docs/JETTIED-STOREYS-CANDIDATE.md b/docs/JETTIED-STOREYS-CANDIDATE.md index 4eaf92e..b6bc27b 100644 --- a/docs/JETTIED-STOREYS-CANDIDATE.md +++ b/docs/JETTIED-STOREYS-CANDIDATE.md @@ -1,5 +1,8 @@ # Jettied storeys — 0.1.2.110021 local handoff +This records the local checks when the candidate was prepared. References to uncommitted +work and hosted checks describe that point; Git history records later commits and merges. + The jetty upgrade is implemented for Minecraft 1.10.2 / Forge 12.18.3.2511. It remains local and uncommitted on feature/1.10-initial-cottage-kit. No feature push, PR, merge or release has been performed. Manual gameplay/visual acceptance diff --git a/docs/LEADED-WINDOWS-CANDIDATE.md b/docs/LEADED-WINDOWS-CANDIDATE.md new file mode 100644 index 0000000..dee28d8 --- /dev/null +++ b/docs/LEADED-WINDOWS-CANDIDATE.md @@ -0,0 +1,97 @@ +# Leaded windows — 0.1.6.110021 + +This records the local checks when the candidate was prepared. References to uncommitted +work and hosted checks describe that point; Git history records later commits and merges. + +Leaded windows now belong to the Late Medieval & Tudor palette. There are 48 window +choices: diamond or rectangular leading, all six woods, raw or limewashed surrounds, +and full or half depth. Two leaded-pane components bring the visible total to 510. + +Matching vertical sections form one outward-opening casement. Side-by-side columns +open separately. Right-click the frame or its open pane, or power any section with +redstone. Manual changes last until the next power change. The whole column checks +outside cells and the swing before moving, so an obstruction cannot leave it half open. +Entity-blocked powered openings retry while waiting; successful manual overrides also +acknowledge the current input, so a pending retry cannot undo them. + +The first section chooses its hinge from the clicked half. Stacked sections inherit +their column's alignment unless sneaking. Thin windows keep the occupied wall half +independent of the outward direction, including when placed from inside the building. +Holding a window in either hand leaves placement available instead of toggling it. +Close adjoining columns before filling a gap between open and closed sections. + +Craft eight glass panes around one iron ingot to make eight diamond-leaded panes. +One pane and a matching principal-post panel make one window, retaining wood, finish +and full/thin depth. Shape the panel before glazing. Windows cannot use the wall-cutting +or rejoining recipes. Panes and windows can swap leading one-for-one, and raw windows +accept single and eight-piece limewashing. The building guide and item hints cover this. + +Windows have their own light-transmitting block family. A hidden outside-cell helper +carries the part of an open pane beyond its owner cell, including collision, targeting +and rendering across chunk borders. It has no item or independent drops. Four metadata +bits hold facing, open and cached power; hidden identities hold right hinges and rear-half +thin placements. Existing saved IDs stay unchanged. There are no tile entities, scheduled +block ticks or Furniture dependency. Idle windows and ownership links are never polled. + +## Verification + +- 41 unit tests pass, including shared window geometry, cell clipping, surface area, + player clearance and rear-half placement. All 967 generated resources are current. +- Build/Javadoc, Java 8 and package audits, Eclipse output synchronization and the + ordinary Eclipse classpath checks pass. Probe classes stay outside player artifacts. +- The packaged server checks 2,304 window metadata states, 576 directional columns + and 144 saved columns on reload. Coverage includes both hinges/depths/finishes/styles, + placement and alignment, both thin half-cell positions, independent adjacent columns, + rejected open/closed bridges, obstruction rollback, entities, hand/power transitions, + manual overrides of pending power edges, owner/helper targeting, player passage, + intact survival/creative drops, explosions, forced replacement and link repair. +- All 2,010 default recipes return their expected output. The crafting matrix covers + 11,444 shifted, mirrored or reordered grids. All 1,514 Architecture-only conversions + preserve separate material quantities, including frame and glazing. All 510 visible + variants are reachable. Extra negative checks reject glazed cutting/rejoining, + cross-braced glazing inputs and mixed wood, finish, depth or leading in wash batches. +- Thirteen packaged server modes pass: core, reload, Furniture coexistence, all families + disabled, windows disabled, three earlier preview imports and five era selections. + Iron Age alone retains 36 recipes. Disabling windows restores 460 variants and 1,863 + recipes. Empty or invalid-only era selections have no Architecture recipes. The + original 4,448-state compatibility matrix remains readable. +- The native client checks models/materials, both actual creative tabs and every visible + item's translated hints. Eighteen captures include closed/open glazing, the interior, + a glazed house beside the unchanged comparison, six woods and a chunk-border casement. +- The new preview is saved and reloaded with only the player mod. A second plain load + needs no missing-probe confirmation. The five earlier ordinary saves remain separate; + all 140 of their existing files retain their original hashes. + +Runtime SHA-256: +`339fbad0d414a5253b0e202976adbb914187b56b39899ef55e8726a3a0371c2a` + +Evidence: `D:\IronAgeArchitecture\manual-testing\windows-0.1.6-qualified-03`. +Earlier attempts remain in the separate early, final and qualified roots. They include +the initial incomplete border-arena failure and a preliminary run that could not bind +the shared local server port. Those attempts are not the final qualification evidence. +The first clean-preview log check also matched Forge's normal connection diagnostic. +Its failed validation record is retained; the corrected check uses the saved-world +missing-probe warning explicitly. Both actual plain server starts succeeded. + +Candidate: `D:\IronAgeArchitecture\candidates\0.1.6.110021-leaded-windows`. +The bundle contains player/sources/Javadoc artifacts, checksums, the building guide, +changelog, source snapshot, preview ZIP, screenshots and hash-bound receipts/logs. +The installed save is **Leaded Windows Preview** in the ordinary project's `run/saves`. +Restart the Eclipse client to load the new code before reviewing it. + +## Gameplay review + +The screenshots have been inspected; automated checks do not replace the player's +review. Try opening from the street and inside, choosing each hinge, stacking sections, +walking through a clear two-block casement and powering a column. Check the window +surrounds alongside thin walls, jetties and the unchanged comparison house. Opening is +instantaneous and reserves an empty outside cell per section. Shutters, bay windows, +half-width sections and clay tiles remain later additions. + +Earlier candidates were not repaired or overwritten. The previous audit records three +exceptions in 0.1.3: `evidence/probe-build.log` and `source-worktree.zip` are missing, and +Windows reports `evidence/wall-head-red.xml` as unreadable/corrupted. The retained audit +is included for context; it is separate from this candidate's own delivery checks. + +This increment remains local and uncommitted on `feature/1.10-era-palettes-and-joins`. +No push, merge or publication has been performed. Hosted CI has not run. diff --git a/docs/RECIPE-PASS-CANDIDATE.md b/docs/RECIPE-PASS-CANDIDATE.md new file mode 100644 index 0000000..7156445 --- /dev/null +++ b/docs/RECIPE-PASS-CANDIDATE.md @@ -0,0 +1,65 @@ +# Crafting — 0.1.5.110021 + +This records the local checks when the candidate was prepared. References to uncommitted +work and hosted checks describe that point; Git history records later commits and merges. + +The wall kit now follows one chain: cob, plain wattle-and-daub infill, timber post panels, +then the chosen framing pattern. Full, thin and short walls use the same arrangements. +The [crafting guide](BUILDING-GUIDE.md#crafting) is the player-facing reference. + +Three matching post panels in a row, column, diagonal or L make three rails, studs, +braces or corner/end panels. Single pieces swap paired designs without additional sticks. +Rows of other wall panels recover base posts, while vertical pairs cut and horizontal +pairs rejoin the wall sizes. Shaped corner pieces can recover their short end posts. +The conversions retain wood and daub finish. + +All raw cob and daub shapes support single or eight-piece limewashing, including slabs +and stairs. Stairs now yield eight from six cob and recover three cob from four stairs. +Slabs, roof ridges and the cut wall/floor sizes can be rejoined without wasting material. +Brackets derive from matching full floors. Translated item hints explain the useful steps. + +No block IDs, saved metadata, geometry or placed-world contents changed in this pass. +The five existing ordinary preview saves remain available; **Jetty Brackets Preview** +can be used to try the new recipes after restarting the Eclipse client. + +## Verification + +- 36 unit tests, build/Javadoc, 771 generated resources, package/Java 8 audits and Eclipse + production classpath checks pass. Normal Eclipse output is synchronized without probes. +- All 1,863 default recipes craft their expected output. The exhaustive placement pass + checks 11,150 shifted, mirrored or reordered grids in the real Forge crafting manager. +- All 1,416 recipes containing only Architecture materials preserve the equivalent + material quantities, including wood, finish, wall size, corner cuts and roof/floor cuts. +- All 460 visible variants are reachable from basic ingredients. The solver respects + wildcard recipe ingredients and repeats under all eight era policy cases. Known wall + starters reject mixed wood. Tests use the actual registered recipes and item metadata. +- Twelve packaged server modes pass: core, reload, five era selections, Furniture + coexistence, all families disabled and the three previous-generation preview imports. + The default/later/mixed era selections contain 1,863 recipes; Iron Age alone has 36; + empty or invalid-only selections have zero. The existing saved-state matrix is unchanged. +- The native client loads the packaged mod, checks every existing model and material, + verifies both era tabs and calls every visible item's tooltip. Hints must be present + and translated. The separate client runtime creates its own disposable preview. + +Runtime SHA-256: +`7d7de0ed475d4d9a60b1f2cd25de9b351f1166fa04efabdca34b600d586bbdc5` + +Evidence: `D:\IronAgeArchitecture\manual-testing\recipes-0.1.5-final`. +The early root retains the failing plain-infill starter contract against 0.1.4 and an +initial reachability-test failure caused by the probe treating wildcard ingredients as +exact metadata. The wildcard correction changes only the verification code. + +Candidate: `D:\IronAgeArchitecture\candidates\0.1.5.110021-crafting`. +The bundle contains runtime/sources/Javadoc artifacts, checksums, the current crafting +guide, source snapshot, screenshots and receipts/logs. It reuses the byte-identical clean +Jetty Brackets Preview ZIP from 0.1.4; this pass changes recipes and hints rather than builds. +Earlier candidates remain separate. Automated verification does not replace gameplay review. + +The earlier-candidate audit verified 166 delivery files and recorded three exceptions in +the 0.1.3 bundle: `evidence/probe-build.log` and `source-worktree.zip` are missing, and +Windows reports `evidence/wall-head-red.xml` as corrupted/unreadable. No earlier files were +modified or restored during this pass. `evidence/earlier-candidate-audit.json` retains the +full results. This does not affect the new candidate's own artifact and receipt checks. + +The work remains local and uncommitted on `feature/1.10-era-palettes-and-joins`. +No push, merge or publication has been performed for this increment. Hosted CI has not run. diff --git a/docs/TIMBER-JOINS-CANDIDATE.md b/docs/TIMBER-JOINS-CANDIDATE.md new file mode 100644 index 0000000..b7607b1 --- /dev/null +++ b/docs/TIMBER-JOINS-CANDIDATE.md @@ -0,0 +1,71 @@ +# Timber joins — 0.1.7.110021 + +This records the local checks when the candidate was prepared. References to uncommitted +work and hosted checks describe that point; Git history records later commits and merges. + +The gaps came from three different places. Some automatic rails extended all the way +across a panel even when they should have stopped at its stud or brace. Diagonal beams +in diagonally adjoining cells met only at a point, leaving a visible pinch where the +other two panel corners were missing. The preview generator also reversed some brace +patterns when it chose the other occupied half of a wall. + +Automatic rails now finish on the timber they meet, using its actual outline at the +rail's height. Short-piece posts move before this calculation. Deliberate sill/head +panels and runs fed from both sides keep their full width. + +Small triangular continuations carry diagonal beams through the adjoining wall corners. +They stay inside their own wall cells, use the matching wood and appear only when two +compatible brace endpoints meet. Removing either brace removes the junction. Different +woods, opposing slopes and offset wall planes cannot create a false join. Collision and +targeting use the same geometry as the rendered pieces. + +The new preview's brace patterns account for the wall's facing, keeping complete bays +aligned across half-block steps. Existing placed patterns retain their selected design; +an incorrectly oriented brace in an older build still needs its pattern swapping. + +No recipes, visible items, saved metadata or registry identities changed. There are +still 510 visible variants and 2,010 default recipes; Iron Age survival retains 36. +The building guide explains the distinction between automatic joins and deliberate rails. + +## Verification + +- Two focused rail-end tests fail against the previous geometry and pass after the fix. + The original failures and corrected results are retained in the evidence bundle. +- All 47 unit tests pass, covering rail ends, both depths, short pieces, unchanged + continuous runs, corner widths, cell bounds, wood selection and outward winding. +- Build/Javadoc, 967 generated resources, package/Java 8 audits and ordinary Eclipse + classpath/output checks pass. Probe code remains outside player artifacts. +- Thirteen packaged server modes pass: core, reload, Furniture coexistence, family + switches, three earlier preview imports and five era selections. The existing window + checks, 4,448-state compatibility matrix and 11,444-grid crafting checks remain green. +- The native world check verifies eight complete preview brace bays and 96 corner + arrangements across six woods, two depths, four facings and both diagonal directions. + Negative controls cover missing endpoints, mixed woods, wrong slopes and offset planes. +- The native client checks the actual models, materials, palettes and item hints and + captures eighteen views. The bundle includes the earlier street view for comparison. +- Timber Joins Preview is saved and reloaded with only the player mod before being + installed alongside the six earlier saves. All 170 files in those saves retain their + original hashes. The frozen 0.1.6 candidate and its screenshots remain unchanged. + +Runtime SHA-256: +`736994ccdcac9134b7691c8060e69ae3b11f779b86e1413dabfe01791b9981dc` + +Evidence: `D:\IronAgeArchitecture\manual-testing\timber-joins-0.1.7-qualified`. +The study root retains the original rail failures and first-pass artifacts. The first +render exposed the point-only diagonal joint; its screenshots and receipts remain in +the separate `timber-joins-0.1.7-final` root. A new corner test initially passed a 2D +outline to a 3D area helper; that test error is retained separately from the final pass. + +Candidate: `D:\IronAgeArchitecture\candidates\0.1.7.110021-timber-joins`. +The bundle contains player/sources/Javadoc artifacts, source snapshot, checksums, +guide, changelog, preview ZIP, screenshots and hash-bound receipts/logs. + +Restart the Eclipse client and choose **Timber Joins Preview** to review the result. +The screenshots have been inspected; gameplay review remains separate from automated +verification. Check the outside and inside joins around the windows, both levels of +the first house, half-step upper floors and the close-studded comparison house. + +This work remains local and uncommitted on `feature/1.10-era-palettes-and-joins`. +No push, merge or publication has been performed. Earlier candidate bundles were not +modified; their previously recorded audit exceptions remain documented in the earlier +handoffs and retained audit report. diff --git a/docs/WATTLE-AND-DAUB-CANDIDATE.md b/docs/WATTLE-AND-DAUB-CANDIDATE.md index bc6ece9..dd11898 100644 --- a/docs/WATTLE-AND-DAUB-CANDIDATE.md +++ b/docs/WATTLE-AND-DAUB-CANDIDATE.md @@ -1,5 +1,8 @@ # Wattle and daub — 0.1.1.110021 local handoff +This records the local checks when the candidate was prepared. References to uncommitted +work and hosted checks describe that point; Git history records later commits and merges. + The wall redesign is implemented for Minecraft 1.10.2 / Forge 12.18.3.2511. Wider Rye building families remain deferred. This candidate remains local and uncommitted on feature/1.10-initial-cottage-kit; there has been no feature push, PR, merge or release. diff --git a/gradle.properties b/gradle.properties index 5458ac9..68ae5db 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.parallel=false net.minecraftforge.gradle.merge-source-sets=false minecraft_version=1.10.2 forge_version=12.18.3.2511 -mod_version=0.1.2.110021 +mod_version=0.1.8.110021 mod_group=zone.moddev.mc mod_id=ironagearchitecture mod_name=Iron Age Architecture diff --git a/scripts/generate-resources.cjs b/scripts/generate-resources.cjs index b6397b2..c8e0b46 100644 --- a/scripts/generate-resources.cjs +++ b/scripts/generate-resources.cjs @@ -15,7 +15,30 @@ const woods = ['oak','spruce','birch','jungle','acacia','dark_oak']; const names = ['Oak','Spruce','Birch','Jungle','Acacia','Dark Oak']; const facings = {north:0,east:90,south:180,west:270}; const shapes = ['straight','inner_left','inner_right','outer_left','outer_right']; -const lang = {'itemGroup.ironagearchitecture':'Iron Age Architecture'}; +const lang = {'itemGroup.ironagearchitecture':'Iron Age Architecture', + 'itemGroup.ironagearchitecture.iron_age':'Iron Age', + 'itemGroup.ironagearchitecture.late_medieval_tudor':'Late Medieval & Tudor'}; +for (const [key, text] of Object.entries({ + 'wall.post':'Shape three matching post panels into a wall pattern.', + 'wall.patterns':'Row: rails; column: studs; diagonal: braces; L: corners.', + 'wall.swap':'Craft alone to swap the paired pattern.', + 'wall.reset':'Three matching pieces in a row return post panels.', + 'wall.corner':'Two shaped corners side by side recover short end posts.', + 'wall.infill':'Full infill + four matching planks makes four post panels.', + 'cut':'Cut a vertical pair; rejoin a horizontal pair.', + 'wash':'Limewash singly, or eight pieces around bone meal.', + 'floor.brackets':'Full floor + one stick makes two matching brackets.', + 'floor.full':'Two full floors vertically make four half floors.', + 'floor.quarter':'Two quarter floors side by side make one half floor.', + 'floor.l':'Craft alone to recover three quarter floors.', + 'bracket.reach':'One full bracket makes two halves; two halves rejoin.', + 'roof.ridge':'Cut two slopes vertically for four ridge caps.', + 'roof.slope':'Two ridge caps side by side make one roof slope.', + 'cob':'Craft alone to make plain wattle and daub.', + 'slab':'Two slabs side by side recover one cob.', + 'wall.short':'Two short walls side by side make one thin wall.', + 'stair':'Four stairs in a square recover three cob.' +})) lang[`hint.${ns}.${key}`] = text; for (const finish of finishes) { const name = finish === 'cob' ? 'Cob' : 'Limewashed Cob'; model(finish,{parent:'minecraft:block/cube_all',textures:{all:`${ns}:blocks/${finish}`}}); @@ -105,6 +128,22 @@ for (const material of ['thatch','slate']) { lang[`tile.${ns}.${name}.name`] = `${material==='thatch'?'Thatch':'Slate'} ${ridge?'Ridge Cap':'Roof'}`; } } +model('window_mesh',{parent:'minecraft:block/block',textures:{particle:'minecraft:blocks/glass_silver'}}); +for(const [w,wood] of woods.entries()) for(const finish of [0,1]) for(const thin of [false,true]) for(const diamond of [true,false]) for(const right of [false,true]) for(const back of [false,true]) { + if(back&&!thin) continue; + const name=`leaded_window_${thin?'thin':'full'}_${finishes[finish]}_${diamond?'diamond':'rectangle'}_${wood}${right?'_right':''}${back?'_back':''}`; + const variants={};for(const facing of Object.keys(facings)) for(const open of [false,true]) for(const powered of [false,true]) variants[`facing=${facing},open=${open},powered=${powered}`]={model:`${ns}:window_mesh`}; + state(name,variants); + if(!right&&!back) {item(name,'window_mesh');lang[`tile.${ns}.${name}.name`]=`${finish?'Limewashed ':''}${names[w]} ${diamond?'Diamond':'Rectangular'} Leaded Window${thin?' (Thin)':''}`;} +} +state('open_window_leaf',Object.fromEntries(Object.keys(facings).map(facing=>[`facing=${facing}`,{model:`${ns}:window_mesh`}]))); +for(let meta=0;meta<2;meta++) {item(`leaded_pane_${meta}`,'window_mesh');lang[`item.${ns}.leaded_pane.${meta}.name`]=`${meta?'Rectangular':'Diamond'} Leaded Pane`;} +lang[`hint.${ns}.window.use`]='Right-click to open the casement; redstone operates the whole column.'; +lang[`hint.${ns}.window.place`]='Stack to share a hinge; sneak to choose a different alignment.'; +lang[`hint.${ns}.window.glaze`]='Craft alone to swap leading. Shape the wall before glazing.'; +lang[`hint.${ns}.pane.use`]='Add to a matching post panel; craft alone to swap leading.'; +lang[`message.${ns}.window.blocked`]='There is not enough clear space to open this window.'; +lang[`message.${ns}.window.join`]='Close the adjoining windows before joining these sections.'; for (const locale of ['en_US','en_GB']) expected.set(`${assets}/lang/${locale}.lang`,Object.entries(lang).map(([k,v])=>`${k}=${v}`).join('\n')+'\n'); expected.set('src/main/resources/mcmod.info',JSON.stringify([{modid:ns,name:'Iron Age Architecture',description:'Cob walls, timber frames, thatch and slate roofs for the Iron Age series.',version:'${version}',mcversion:'${mcversion}',url:'https://github.com/MinecraftModDevelopmentMods/IronAgeArchitecture',authorList:['SkyBlade1978','FoxNekitsune'],credits:'Original 2019 scaffold and model drafts retained in repository history.',dependencies:[]}],null,2)+'\n'); expected.set('src/main/resources/pack.mcmeta',JSON.stringify({pack:{pack_format:2,description:'Iron Age Architecture resources'}},null,2)+'\n'); @@ -136,4 +175,4 @@ for(const [file,content] of expected) if(file.includes('/models/')&&file.endsWit for(const texture of Object.values(value.textures||{})) if(texture.startsWith(ns+':')&&!fs.existsSync(path.join(root,`${assets}/textures/${texture.split(':')[1]}.png`))) failures.push('missing texture '+texture); } if(failures.length) {console.error(failures.join('\n'));process.exitCode=1;} -else console.log(`${check?'Verified':'Generated'} ${expected.size} resources; ${Object.keys(lang).length-1} visible block variants.`); +else console.log(`${check?'Verified':'Generated'} ${expected.size} resources; ${Object.keys(lang).filter(k=>/^(tile|item)\./.test(k)&&k.endsWith('.name')).length} visible item variants.`); diff --git a/scripts/invoke-client-preview.ps1 b/scripts/invoke-client-preview.ps1 index 3d18408..fb77121 100644 --- a/scripts/invoke-client-preview.ps1 +++ b/scripts/invoke-client-preview.ps1 @@ -1,6 +1,7 @@ param( - [string]$Runtime='D:\IronAgeArchitecture\manual-testing\jetties-0.1.2\native-client', - [string]$ServerTemplate='D:\IronAgeFurniture\manual-testing\sylvester-iaf-0.3.0.110021-20260921', + [string]$Runtime='D:\IronAgeArchitecture\manual-testing\horizontal-rails-0.1.8\native-client', + [string]$PreviewName='Horizontal Rails Preview', + [string]$ServerTemplate='D:\IronAgeArchitecture\manual-testing\jetties-0.1.2-final\core', [string]$GradleHome='D:\GradleCaches\minecraft-mods-shared', [string]$AssetsDirectory='', [string]$Java8='C:\Users\johnb\.gradle\jdks\temurin-8-amd64-windows.2\bin\java.exe' @@ -52,7 +53,7 @@ guiScale:2 fancyGraphics:true '@ | Set-Content -LiteralPath (Join-Path $Runtime 'options.txt') -Encoding ascii $digest=(Get-FileHash -LiteralPath (Join-Path $Runtime ('mods/'+$runtimeJar)) -Algorithm SHA256).Hash.ToLowerInvariant() -$args=@('-Xmx2G',('-Djava.library.path='+(Join-Path $slime 'natives')),'-Diaa.probe=true','-Diaa.clientProbe=true','-Diaa.keepRunning=true',('-Diaa.runtimeSha256='+$digest),'-cp',($classpath -join ';'), +$args=@('-Xmx2G',('-Djava.library.path='+(Join-Path $slime 'natives')),'-Diaa.probe=true','-Diaa.clientProbe=true','-Diaa.keepRunning=true',('-Diaa.previewName='+$PreviewName),('-Diaa.runtimeSha256='+$digest),'-cp',($classpath -join ';'), 'net.minecraft.launchwrapper.Launch','--tweakClass','net.minecraftforge.fml.common.launcher.FMLTweaker', '--username','ArchitecturePreview','--version','1.10.2','--gameDir',$Runtime,'--assetsDir',$AssetsDirectory, '--assetIndex','1.10','--uuid','00000000000000000000000000000001','--accessToken','0','--userType','legacy','--versionType','release','--width','1280','--height','800') diff --git a/scripts/invoke-runtime-probe.ps1 b/scripts/invoke-runtime-probe.ps1 index efe81fb..dd692e5 100644 --- a/scripts/invoke-runtime-probe.ps1 +++ b/scripts/invoke-runtime-probe.ps1 @@ -1,7 +1,8 @@ param( - [ValidateSet('core','reload','furniture','disabled','legacy','legacy-daub')][string]$Mode='core', - [string]$RuntimeRoot='D:\IronAgeArchitecture\manual-testing\jetties-0.1.2', - [string]$ServerTemplate='D:\IronAgeFurniture\manual-testing\sylvester-iaf-0.3.0.110021-20260921', + [ValidateSet('core','reload','furniture','disabled','legacy','legacy-daub','legacy-jetties','iron-age','late-medieval','mixed-eras','no-eras','invalid-era','windows-disabled')][string]$Mode='core', + [string]$RuntimeRoot='D:\IronAgeArchitecture\manual-testing\horizontal-rails-0.1.8', + [string]$ServerTemplate='D:\IronAgeArchitecture\manual-testing\jetties-0.1.2-final\core', + [string]$FurnitureJar='D:\IronAgeArchitecture\manual-testing\jetties-0.1.2-final\furniture\mods\iron-age-furniture-0.3.0.110021.jar', [string]$Java8='C:\Users\johnb\.gradle\jdks\temurin-8-amd64-windows.2\bin\java.exe' ) $ErrorActionPreference='Stop' @@ -32,7 +33,23 @@ if($Mode -eq 'legacy-daub') { Copy-Item -LiteralPath (Join-Path $staging 'Wattle and Daub Preview') -Destination (Join-Path $folder 'world') -Recurse } if($Mode -eq 'furniture') { - Copy-Item -LiteralPath (Join-Path $ServerTemplate 'mods/iron-age-furniture-0.3.0.110021.jar') -Destination (Join-Path $folder 'mods') + Copy-Item -LiteralPath $FurnitureJar -Destination (Join-Path $folder 'mods') +} +if($Mode -eq 'legacy-jetties') { + if(Test-Path -LiteralPath (Join-Path $folder 'world')) {throw 'Legacy jetty probe requires a fresh disposable destination'} + Add-Type -AssemblyName System.IO.Compression.FileSystem + $staging=Join-Path $folder 'jetty-preview-source' + [System.IO.Compression.ZipFile]::ExtractToDirectory('D:\IronAgeArchitecture\candidates\0.1.2.110021-jettied-storeys\Jettied Street Preview.zip',$staging) + Copy-Item -LiteralPath (Join-Path $staging 'Jettied Street Preview') -Destination (Join-Path $folder 'world') -Recurse +} +$eraModes=@{'iron-age'=@('iron_age');'late-medieval'=@('late_medieval_tudor');'mixed-eras'=@('iron_age','late_medieval_tudor');'no-eras'=@();'invalid-era'=@('misspelled_era')} +if($eraModes.ContainsKey($Mode)) { + New-Item -ItemType Directory -Force -Path (Join-Path $folder 'config') | Out-Null + ("recipes {`n S:survivalEras <`n"+($eraModes[$Mode] -join "`n")+"`n >`n}`n") | Set-Content -LiteralPath (Join-Path $folder 'config/ironagearchitecture.cfg') -Encoding ascii +} +if($Mode -eq 'windows-disabled') { + New-Item -ItemType Directory -Force -Path (Join-Path $folder 'config') | Out-Null + "blocks {`n B:leadedWindows=false`n}" | Set-Content -LiteralPath (Join-Path $folder 'config/ironagearchitecture.cfg') -Encoding ascii } if($Mode -eq 'disabled') { New-Item -ItemType Directory -Force -Path (Join-Path $folder 'config') | Out-Null @@ -67,6 +84,8 @@ $args+=('-Diaa.runtimeSha256='+(Get-FileHash -LiteralPath (Join-Path $folder ('m if($Mode -eq 'reload') {$args+='-Diaa.reload=true'} if($Mode -eq 'legacy') {$args+='-Diaa.legacy=true'} if($Mode -eq 'legacy-daub') {$args+='-Diaa.legacyDaub=true'} +if($Mode -eq 'legacy-jetties') {$args+='-Diaa.legacyJetties=true'} +if($Mode -eq 'core') {$args+=@('-Diaa.eraMatrix=true','-Diaa.recipeMatrix=true')} if($Mode -eq 'disabled') {$args+='-Diaa.expectedVariants=0'} $args+=@('-jar',$forge,'nogui') Push-Location $folder diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/ArchitectureBlocks.java b/src/main/java/zone/moddev/mc/ironagearchitecture/ArchitectureBlocks.java index d99ff74..74cb1cc 100644 --- a/src/main/java/zone/moddev/mc/ironagearchitecture/ArchitectureBlocks.java +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/ArchitectureBlocks.java @@ -9,6 +9,7 @@ import net.minecraft.item.ItemBlock; import net.minecraft.item.ItemSlab; import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; import net.minecraft.util.EnumFacing; import net.minecraft.util.ResourceLocation; import net.minecraftforge.fml.common.registry.GameRegistry; @@ -34,6 +35,16 @@ public final class ArchitectureBlocks { public static final List PARTIALS = new ArrayList<>(); public static final List FLOORS = new ArrayList<>(); public static final List BRACKETS = new ArrayList<>(); + public static final List WINDOWS = new ArrayList<>(); + public static WindowLeafBlock WINDOW_LEAF; + public static LeadedPaneItem LEADED_PANE; + public static List visibleStacks() { + List result=new ArrayList<>(); + for(Block block:VISIBLE) for(int meta=0;meta<(block instanceof FrameBlock?((FrameBlock)block).variants():1);meta++) result.add(new ItemStack(block,1,meta)); + for(JettyBracketItem item:BRACKETS) result.add(new ItemStack(item)); + if(LEADED_PANE!=null) {result.add(new ItemStack(LEADED_PANE,1,0));result.add(new ItemStack(LEADED_PANE,1,1));} + return result; + } public static List meshes() { List result=new ArrayList<>(WALLS);result.addAll(PARTIALS);result.addAll(FLOORS);return result; } @@ -55,15 +66,15 @@ private static void item(Block block, ItemBlock item) { public static void register() { if (ArchitectureConfig.cob) for (String finish : FINISHES) { Block full = block(new CobBlock(), finish); - item(full, new ItemBlock(full)); + item(full, new EraItemBlock(full)); COB.add(full); CobSlab half = block(new CobSlab(false), finish + "_slab"); CobSlab doubled = block(new CobSlab(true), finish + "_double_slab"); half.setHalf(half); doubled.setHalf(half); - item(half, new ItemSlab(half, half, doubled)); + item(half, new EraItemSlab(half, half, doubled)); SLABS.add(half); CobStairs stairs = block(new CobStairs(full.getDefaultState()), finish + "_stairs"); - item(stairs, new ItemBlock(stairs)); + item(stairs, new EraItemBlock(stairs)); STAIRS.add(stairs); } if (ArchitectureConfig.frames) { @@ -74,9 +85,29 @@ public static void register() { if(family==FrameFamily.POST) FRAMES.add(frame); } registerJetties(); + if(ArchitectureConfig.windows) registerWindows(); } if (ArchitectureConfig.thatch) roofFamily("thatch", true); if (ArchitectureConfig.slate) roofFamily("slate", false); + EraCatalog.assignPrimaryTabs(); + } + private static void registerWindows() { + WINDOW_LEAF=block(new WindowLeafBlock(),"open_window_leaf"); + LEADED_PANE=new LeadedPaneItem();LEADED_PANE.setRegistryName(new ResourceLocation(IronAgeArchitecture.MODID,"leaded_pane")); + LEADED_PANE.setUnlocalizedName(IronAgeArchitecture.MODID+".leaded_pane");LEADED_PANE.setCreativeTab(EraTabs.tab(Era.LATE_MEDIEVAL_TUDOR));GameRegistry.register(LEADED_PANE); + for(String wood:WOODS) for(int finish=0;finish<2;finish++) for(boolean thin:new boolean[]{false,true}) for(boolean diamond:new boolean[]{true,false}) for(boolean right:new boolean[]{false,true}) for(boolean back:new boolean[]{false,true}) { + if(back&&!thin) continue; + String name="leaded_window_"+(thin?"thin_":"full_")+FINISHES[finish]+"_"+(diamond?"diamond_":"rectangle_")+wood+(right?"_right":"")+(back?"_back":""); + WindowBlock window=block(new WindowBlock(wood,finish,thin,diamond,right,back),name);WINDOWS.add(window); + if(!right&&!back) item(window,new WindowItem(window));Blocks.FIRE.setFireInfo(window,5,20); + } + } + public static WindowBlock findWindow(String wood,int finish,boolean thin,boolean diamond,boolean right) { + return findWindow(wood,finish,thin,diamond,right,false); + } + public static WindowBlock findWindow(String wood,int finish,boolean thin,boolean diamond,boolean right,boolean back) { + for(WindowBlock block:WINDOWS) if(block.wood.equals(wood)&&block.finish==finish&&block.thin==thin&&block.diamond==diamond&&block.right==right&&block.back==back) return block; + throw new IllegalArgumentException("Unregistered window "+wood+" "+finish+" "+thin+" "+diamond+" "+right); } private static FrameBlock wall(String wood,FrameFamily family) { FrameBlock frame=block(new FrameBlock(wood,family),family.registryPrefix+(wood==null?"":"_"+wood)); @@ -137,7 +168,7 @@ public static JettyBracketItem findBracket(String wood,boolean full) { private static void roofFamily(String name, boolean flammable) { for (boolean ridge : new boolean[]{false, true}) { RoofBlock roof = block(ridge ? new RidgeBlock(flammable) : new RoofBlock(false, flammable), name + (ridge ? "_ridge" : "_roof")); - item(roof, new ItemBlock(roof)); + item(roof, new EraItemBlock(roof)); ROOFS.add(roof); if (flammable) Blocks.FIRE.setFireInfo(roof, 60, 20); } diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/ArchitectureConfig.java b/src/main/java/zone/moddev/mc/ironagearchitecture/ArchitectureConfig.java index 393adb6..7422286 100644 --- a/src/main/java/zone/moddev/mc/ironagearchitecture/ArchitectureConfig.java +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/ArchitectureConfig.java @@ -5,15 +5,22 @@ /** Complete content-family switches. Framed panels require the cob family. */ public final class ArchitectureConfig { - public static boolean cob, frames, thatch, slate; + public static boolean cob, frames, thatch, slate,windows; + public static EraSelection survivalEras=EraSelection.parse("all"); private ArchitectureConfig() { } public static void load(File file) { Configuration config = new Configuration(file); config.load(); cob = config.getBoolean("cob", "blocks", true, "Raw and limewashed cob blocks, stairs and slabs."); frames = config.getBoolean("timberFrames", "blocks", true, "All six timber frames; also requires cob.") && cob; + windows = config.getBoolean("leadedWindows", "blocks", true, "Openable leaded windows; requires timberFrames and cob.") && frames; thatch = config.getBoolean("thatch", "blocks", true, "Thatch slopes and ridge caps."); slate = config.getBoolean("slate", "blocks", true, "Crafted slate slopes and ridge caps."); + net.minecraftforge.common.config.Property eras=config.get("recipes","survivalEras",new String[]{"all"}, + "Allowed survival palettes: all, iron_age, late_medieval_tudor. Select one or more IDs. An empty list disables Architecture recipes. Shared materials stay craftable when any selected era uses them. Does not hide creative items or remove existing blocks. Restart required."); + eras.setRequiresMcRestart(true); + survivalEras=EraSelection.parse(eras.getStringList()); + for(String unknown:survivalEras.unknown) org.apache.logging.log4j.LogManager.getLogger("IronAgeArchitecture").warn("Ignoring unknown survival era '{}'. Valid IDs: iron_age, late_medieval_tudor, all",unknown); if (config.hasChanged()) config.save(); } } diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/ArchitectureRecipes.java b/src/main/java/zone/moddev/mc/ironagearchitecture/ArchitectureRecipes.java index 0aeeef4..22bc536 100644 --- a/src/main/java/zone/moddev/mc/ironagearchitecture/ArchitectureRecipes.java +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/ArchitectureRecipes.java @@ -5,116 +5,146 @@ import net.minecraft.init.Items; import net.minecraft.item.ItemStack; import net.minecraftforge.fml.common.registry.GameRegistry; -import zone.moddev.mc.ironagearchitecture.block.FrameBlock; -import zone.moddev.mc.ironagearchitecture.block.FrameFamily; -import zone.moddev.mc.ironagearchitecture.block.RoofBlock; import zone.moddev.mc.ironagearchitecture.block.*; -/** Vanilla-only survival recipes, with explicit wood and finish identities. */ +/** Shared starters, lossless shaping and finish-preserving conversions. */ public final class ArchitectureRecipes { private ArchitectureRecipes() { } + private static void shaped(ItemStack output,Object... inputs) {if(EraCatalog.canCraft(output)) GameRegistry.addRecipe(output,inputs);} + private static void shapeless(ItemStack output,Object... inputs) {if(EraCatalog.canCraft(output)) GameRegistry.addShapelessRecipe(output,inputs);} + private static ItemStack stack(Block block,int count,int meta) {return new ItemStack(block,count,meta);} + private static ItemStack count(ItemStack stack,int count) {ItemStack result=stack.copy();result.stackSize=count;return result;} public static void register() { - if (ArchitectureConfig.cob) { - GameRegistry.addShapelessRecipe(new ItemStack(ArchitectureBlocks.COB.get(0),8), - Blocks.DIRT,Blocks.DIRT,Blocks.DIRT,Blocks.DIRT,Items.CLAY_BALL,Items.CLAY_BALL,Items.CLAY_BALL,Items.CLAY_BALL,Items.WHEAT); - GameRegistry.addShapelessRecipe(new ItemStack(ArchitectureBlocks.COB.get(1)),ArchitectureBlocks.COB.get(0),new ItemStack(Items.DYE,1,15)); - for (int finish = 0; finish < 2; finish++) { - Block cob = ArchitectureBlocks.COB.get(finish); - GameRegistry.addRecipe(new ItemStack(ArchitectureBlocks.SLABS.get(finish),6),"CCC",'C',cob); - GameRegistry.addRecipe(new ItemStack(ArchitectureBlocks.STAIRS.get(finish),4),"C ","CC ","CCC",'C',cob); + if(ArchitectureConfig.cob) registerCob(); + if(ArchitectureConfig.frames) { + registerWalls();registerWallCuts();registerCorners();registerFloors(); + for(Block block:ArchitectureBlocks.VISIBLE) if(block instanceof FrameBlock&&!(block instanceof JettyFloorBlock)) { + FrameBlock wall=(FrameBlock)block; + int patterns=wall.variants()/2; + for(int pattern=0;pattern eras(Item item) { + if(item==null||item.getRegistryName()==null||!IronAgeArchitecture.MODID.equals(item.getRegistryName().getResourceDomain())) return EnumSet.noneOf(Era.class); + if(item instanceof JettyBracketItem||item instanceof LeadedPaneItem) return EnumSet.of(Era.LATE_MEDIEVAL_TUDOR); + if(!(item instanceof ItemBlock)) return EnumSet.noneOf(Era.class); + Block block=((ItemBlock)item).getBlock(); + boolean shared=ArchitectureBlocks.COB.contains(block)||ArchitectureBlocks.SLABS.contains(block)||ArchitectureBlocks.STAIRS.contains(block) + ||block instanceof FrameBlock&&((FrameBlock)block).family==FrameFamily.INFILL + ||block instanceof RoofBlock&&block.getRegistryName().getResourcePath().startsWith("thatch"); + return shared?EnumSet.allOf(Era.class):EnumSet.of(Era.LATE_MEDIEVAL_TUDOR); + } + public static boolean canCraft(ItemStack output) {return output!=null&&ArchitectureConfig.survivalEras.permits(eras(output.getItem()));} + public static void assignPrimaryTabs() { + for(Block block:ArchitectureBlocks.VISIBLE) { + Item item=Item.getItemFromBlock(block); + Era era=eras(item).iterator().next(); + block.setCreativeTab(EraTabs.tab(era));item.setCreativeTab(EraTabs.tab(era)); + } + for(JettyBracketItem item:ArchitectureBlocks.BRACKETS) item.setCreativeTab(EraTabs.tab(Era.LATE_MEDIEVAL_TUDOR)); + } +} diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/EraSelection.java b/src/main/java/zone/moddev/mc/ironagearchitecture/EraSelection.java new file mode 100644 index 0000000..8cf3332 --- /dev/null +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/EraSelection.java @@ -0,0 +1,25 @@ +package zone.moddev.mc.ironagearchitecture; + +import java.util.*; + +/** Restart-time survival selection. Unknown IDs never silently enable an era. */ +public final class EraSelection { + public final Set allowed; + public final List unknown; + private EraSelection(EnumSet allowed,List unknown) { + this.allowed=Collections.unmodifiableSet(allowed);this.unknown=Collections.unmodifiableList(unknown); + } + public static EraSelection parse(String... values) { + EnumSet allowed=EnumSet.noneOf(Era.class);List unknown=new ArrayList<>(); + for(String input:values) { + String id=input.trim().toLowerCase(Locale.ROOT); + if(id.isEmpty()) continue; + if(id.equals("all")||id.equals("*")) {allowed.addAll(EnumSet.allOf(Era.class));continue;} + boolean found=false; + for(Era era:Era.values()) if(era.id.equals(id)) {allowed.add(era);found=true;break;} + if(!found) unknown.add(input); + } + return new EraSelection(allowed,unknown); + } + public boolean permits(Set membership) {return !Collections.disjoint(allowed,membership);} +} diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/EraTabs.java b/src/main/java/zone/moddev/mc/ironagearchitecture/EraTabs.java new file mode 100644 index 0000000..1cfd9f5 --- /dev/null +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/EraTabs.java @@ -0,0 +1,23 @@ +package zone.moddev.mc.ironagearchitecture; + +import java.util.EnumMap; +import net.minecraft.creativetab.CreativeTabs; +import net.minecraft.init.Blocks; +import net.minecraft.item.Item; + +/** Creative availability is independent of the server's survival recipe selection. */ +public final class EraTabs { + private static final EnumMap TABS=new EnumMap<>(Era.class); + static { + for(final Era era:Era.values()) TABS.put(era,new CreativeTabs("ironagearchitecture."+era.id) { + @Override public Item getTabIconItem() { + if(era==Era.LATE_MEDIEVAL_TUDOR&&!ArchitectureBlocks.FRAMES.isEmpty()) return Item.getItemFromBlock(ArchitectureBlocks.FRAMES.get(0)); + if(!ArchitectureBlocks.COB.isEmpty()) return Item.getItemFromBlock(ArchitectureBlocks.COB.get(0)); + return Item.getItemFromBlock(Blocks.BRICK_BLOCK); + } + }); + } + private EraTabs() { } + public static CreativeTabs tab(Era era) {return TABS.get(era);} + public static CreativeTabs[] forItem(Item item) {return EraCatalog.eras(item).stream().map(EraTabs::tab).toArray(CreativeTabs[]::new);} +} diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/IronAgeArchitecture.java b/src/main/java/zone/moddev/mc/ironagearchitecture/IronAgeArchitecture.java index 45019d8..e7b4425 100644 --- a/src/main/java/zone/moddev/mc/ironagearchitecture/IronAgeArchitecture.java +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/IronAgeArchitecture.java @@ -1,27 +1,21 @@ package zone.moddev.mc.ironagearchitecture; import net.minecraft.creativetab.CreativeTabs; -import net.minecraft.init.Blocks; -import net.minecraft.item.Item; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.SidedProxy; import net.minecraftforge.fml.common.event.FMLInitializationEvent; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; /** Permanent building components in the Iron Age series. */ -@Mod(modid = IronAgeArchitecture.MODID, name = "Iron Age Architecture", version = "0.1.2.110021", +@Mod(modid = IronAgeArchitecture.MODID, name = "Iron Age Architecture", version = "0.1.8.110021", acceptedMinecraftVersions = "[1.10.2]", dependencies = "required-after:Forge@[12.18.3.2511,)") public final class IronAgeArchitecture { public static final String MODID = "ironagearchitecture"; @SidedProxy(clientSide = "zone.moddev.mc.ironagearchitecture.client.ClientProxy", serverSide = "zone.moddev.mc.ironagearchitecture.CommonProxy") public static CommonProxy proxy; - public static final CreativeTabs TAB = new CreativeTabs(MODID) { - @Override public Item getTabIconItem() { - return ArchitectureBlocks.COB.isEmpty() ? Item.getItemFromBlock(Blocks.BRICK_BLOCK) - : Item.getItemFromBlock(ArchitectureBlocks.COB.get(0)); - } - }; + /** Compatibility alias; palette membership now controls the actual creative tabs. */ + @Deprecated public static final CreativeTabs TAB = EraTabs.tab(Era.LATE_MEDIEVAL_TUDOR); @Mod.EventHandler public void preInit(FMLPreInitializationEvent event) { ArchitectureConfig.load(event.getSuggestedConfigurationFile()); ArchitectureBlocks.register(); diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/EraItemBlock.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/EraItemBlock.java new file mode 100644 index 0000000..a1f1d93 --- /dev/null +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/EraItemBlock.java @@ -0,0 +1,22 @@ +package zone.moddev.mc.ironagearchitecture.block; + +import net.minecraft.block.Block; +import net.minecraft.item.ItemBlock; +import net.minecraft.creativetab.CreativeTabs; +import zone.moddev.mc.ironagearchitecture.EraTabs; +import zone.moddev.mc.ironagearchitecture.ArchitectureBlocks; +import net.minecraft.item.ItemStack; +import net.minecraft.entity.player.EntityPlayer; +import java.util.List; + +public class EraItemBlock extends ItemBlock { + public EraItemBlock(Block block) {super(block);} + @Override public CreativeTabs[] getCreativeTabs() {return EraTabs.forItem(this);} + protected static void hint(List tooltip,String key) {tooltip.add(net.minecraft.util.text.translation.I18n.translateToLocal("hint.ironagearchitecture."+key));} + @Override public void addInformation(ItemStack stack,EntityPlayer player,List tooltip,boolean advanced) { + if(block instanceof RoofBlock) hint(tooltip,((RoofBlock)block).isRidge()?"roof.slope":"roof.ridge"); + if(ArchitectureBlocks.COB.contains(block)) hint(tooltip,"cob"); + if(ArchitectureBlocks.STAIRS.contains(block)) hint(tooltip,"stair"); + if(ArchitectureBlocks.COB.indexOf(block)==0||ArchitectureBlocks.STAIRS.indexOf(block)==0) hint(tooltip,"wash"); + } +} diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/EraItemSlab.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/EraItemSlab.java new file mode 100644 index 0000000..0029452 --- /dev/null +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/EraItemSlab.java @@ -0,0 +1,15 @@ +package zone.moddev.mc.ironagearchitecture.block; + +import net.minecraft.block.*; +import net.minecraft.item.ItemSlab; +import net.minecraft.creativetab.CreativeTabs; +import zone.moddev.mc.ironagearchitecture.EraTabs; + +public final class EraItemSlab extends ItemSlab { + public EraItemSlab(Block block,BlockSlab half,BlockSlab doubled) {super(block,half,doubled);} + @Override public CreativeTabs[] getCreativeTabs() {return EraTabs.forItem(this);} + @Override public void addInformation(net.minecraft.item.ItemStack stack,net.minecraft.entity.player.EntityPlayer player,java.util.List tooltip,boolean advanced) { + EraItemBlock.hint(tooltip,"slab"); + if(zone.moddev.mc.ironagearchitecture.ArchitectureBlocks.SLABS.indexOf(block)==0) EraItemBlock.hint(tooltip,"wash"); + } +} diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/FrameBlock.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/FrameBlock.java index 7a4a281..6402617 100644 --- a/src/main/java/zone/moddev/mc/ironagearchitecture/block/FrameBlock.java +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/FrameBlock.java @@ -45,7 +45,7 @@ public FrameBlock(String wood,FrameFamily family) { } public int variants() { return family==FrameFamily.INFILL?2:4; } public int finish(int panel) { return family==FrameFamily.INFILL?panel&1:panel>>1; } - @Override protected BlockStateContainer createBlockState() { return new ExtendedBlockState(this,new IProperty[]{FACING,PANEL},new IUnlistedProperty[]{CONNECTIONS}); } + @Override protected BlockStateContainer createBlockState() { return new ExtendedBlockState(this,new IProperty[]{FACING,PANEL},new IUnlistedProperty[]{CONNECTIONS,TimberCorners.PROPERTY}); } @Override public IBlockState getStateFromMeta(int meta) { return getDefaultState().withProperty(FACING, EnumFacing.getHorizontal(meta & 3)).withProperty(PANEL, ((meta >> 2) & 3)%variants()); } @Override public int getMetaFromState(IBlockState state) { return state.getValue(FACING).getHorizontalIndex() | (state.getValue(PANEL) << 2); } @Override public int damageDropped(IBlockState state) { return state.getValue(PANEL)%variants(); } @@ -72,6 +72,11 @@ private int railRequest(IBlockAccess world,BlockPos pos,EnumFacing direction,Enu IBlockState previous=world.getBlockState(pos);BlockPos previousPos=pos; for(int distance=1;distance<=16;distance++) { IBlockState neighbour=world.getBlockState(pos.offset(direction,distance)); + if(neighbour.getBlock() instanceof WindowBlock) { + WindowBlock window=(WindowBlock)neighbour.getBlock();BlockPos at=pos.offset(direction,distance); + if(wood==null||!wood.equals(window.wood)||!WallRuns.touches(previous,previousPos,neighbour,at,direction)) return 0; + int joins=window.joins(neighbour,world,at);return ((joins&WindowGeometry.DOWN)==0?1:0)|((joins&WindowGeometry.UP)==0?2:0); + } if(!(neighbour.getBlock() instanceof FrameBlock)) return 0; FrameBlock block=(FrameBlock)neighbour.getBlock(); if(block instanceof JettyFloorBlock) return 0; @@ -90,6 +95,7 @@ public int connections(IBlockState state,IBlockAccess world,BlockPos pos) { for(EnumFacing direction:EnumFacing.HORIZONTALS) { IBlockState neighbour=world.getBlockState(pos.offset(direction)); if(neighbour.getBlock() instanceof FrameBlock&&!(neighbour.getBlock() instanceof JettyFloorBlock)) mask|=WallGeometry.bit(direction); + if(neighbour.getBlock() instanceof WindowBlock&&WallRuns.touches(state,pos,neighbour,pos.offset(direction),direction)) mask|=WallGeometry.bit(direction); if(family!=FrameFamily.INFILL) { EnumFacing plane=direction.getAxis()==facing.getAxis()?facing.rotateY():facing; int request=railRequest(world,pos,direction,plane); @@ -97,6 +103,7 @@ public int connections(IBlockState state,IBlockAccess world,BlockPos pos) { if((request&2)!=0) mask|=WallGeometry.bit(direction)<<8; } } + mask=RailJoins.bridge(state,world,pos,mask); if(family==FrameFamily.RAIL) { mask|=verticalStud(world,pos.down(),facing)<<12; mask|=verticalStud(world,pos.up(),facing)<<16; @@ -130,9 +137,15 @@ private int braceJoins(IBlockAccess world,BlockPos pos,EnumFacing plane) { return result; } @Override public IBlockState getExtendedState(IBlockState state,IBlockAccess world,BlockPos pos) { - return ((IExtendedBlockState)state).withProperty(CONNECTIONS,connections(state,world,pos)); + return ((IExtendedBlockState)state).withProperty(CONNECTIONS,connections(state,world,pos)).withProperty(TimberCorners.PROPERTY,TimberCorners.derive(state,world,pos)); } public List parts(IBlockState state,int mask) { return WallGeometry.parts(family,state.getValue(PANEL),state.getValue(FACING),mask); } + public List parts(IBlockState state,int mask,int corners) { + return TimberCorners.add(parts(state,mask),this instanceof PartialWallBlock,corners); + } + private List worldParts(IBlockState state,IBlockAccess world,BlockPos pos) { + return parts(state,connections(state,world,pos),TimberCorners.derive(state,world,pos)); + } @Override public boolean isOpaqueCube(IBlockState state) { return false; } // These are filled wall cells. Forge 1.10 must sample the neighbouring cell for // face light even though the plaster has a small cosmetic recess. Opacity still @@ -141,15 +154,15 @@ private int braceJoins(IBlockAccess world,BlockPos pos,EnumFacing plane) { @Override public boolean isSideSolid(IBlockState state,IBlockAccess world,BlockPos pos,EnumFacing side) { return true; } @Override public AxisAlignedBB getBoundingBox(IBlockState state,IBlockAccess world,BlockPos pos) { AxisAlignedBB result=null; - for(WallGeometry.Part part:parts(state,connections(state,world,pos))) result=result==null?part.bounds(state.getValue(FACING)):result.union(part.bounds(state.getValue(FACING))); + for(WallGeometry.Part part:worldParts(state,world,pos)) result=result==null?part.bounds(state.getValue(FACING)):result.union(part.bounds(state.getValue(FACING))); return result; } @Override public void addCollisionBoxToList(IBlockState state,World world,BlockPos pos,AxisAlignedBB mask,List boxes,@Nullable Entity entity) { - for(WallGeometry.Part part:parts(state,connections(state,world,pos))) addCollisionBoxToList(pos,mask,boxes,part.bounds(state.getValue(FACING))); + for(WallGeometry.Part part:worldParts(state,world,pos)) addCollisionBoxToList(pos,mask,boxes,part.bounds(state.getValue(FACING))); } @Nullable @Override public RayTraceResult collisionRayTrace(IBlockState state,World world,BlockPos pos,Vec3d start,Vec3d end) { RayTraceResult result=null;double closest=Double.POSITIVE_INFINITY; - for(WallGeometry.Part part:parts(state,connections(state,world,pos))) { + for(WallGeometry.Part part:worldParts(state,world,pos)) { RayTraceResult hit=rayTrace(pos,start,end,part.bounds(state.getValue(FACING))); if(hit!=null&&start.squareDistanceTo(hit.hitVec) tooltip,boolean advanced) {tooltip.add("Use beneath a matching jetty floor edge.");} + @Override public void addInformation(ItemStack stack,EntityPlayer player,List tooltip,boolean advanced) { + tooltip.add("Use beneath a matching jetty floor edge.");EraItemBlock.hint(tooltip,"bracket.reach"); + } } diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/JettyFloorBlock.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/JettyFloorBlock.java index 43329c1..62fb32b 100644 --- a/src/main/java/zone/moddev/mc/ironagearchitecture/block/JettyFloorBlock.java +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/JettyFloorBlock.java @@ -88,7 +88,26 @@ public boolean covers(IBlockState state,double x,double z) { if(wood.equals(block.wood)&&block.covers(next,p[0]-dx*16,p[2]-dz*16)) mask|=1<<(q*4+side.getHorizontalIndex()); } } - return mask; + IBlockState below=world.getBlockState(pos.down()); + int support=0; + if(below.getBlock() instanceof FrameBlock&&!(below.getBlock() instanceof JettyFloorBlock)||below.getBlock() instanceof WindowBlock) { + for(int q=0;q<4;q++) { + double[] point=WallGeometry.rotate(q%2*8+4,0,q/2*8+4,facing); + boolean occupied=true; + if(below.getBlock() instanceof WindowBlock&&((WindowBlock)below.getBlock()).thin) { + EnumFacing turn=below.getValue(WindowBlock.FACING),inverse=turn==EnumFacing.EAST?EnumFacing.WEST:turn==EnumFacing.WEST?EnumFacing.EAST:turn; + occupied=(WallGeometry.rotate(point[0],0,point[2],inverse)[2]>=8)==((WindowBlock)below.getBlock()).back; + } + if(below.getBlock() instanceof PartialWallBlock) { + EnumFacing turn=below.getValue(FACING); + EnumFacing inverse=turn==EnumFacing.EAST?EnumFacing.WEST:turn==EnumFacing.WEST?EnumFacing.EAST:turn; + double[] local=WallGeometry.rotate(point[0],0,point[2],inverse); + occupied=((PartialWallBlock)below.getBlock()).profile.contains(local[0],local[2]); + } + if(occupied) support|=1< parts(IBlockState state,int joins) { boolean full=(state.getValue(PANEL)&2)!=0; diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/JettyFloorGeometry.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/JettyFloorGeometry.java index 49473ff..198252d 100644 --- a/src/main/java/zone/moddev/mc/ironagearchitecture/block/JettyFloorGeometry.java +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/JettyFloorGeometry.java @@ -14,6 +14,13 @@ public static boolean exposed(FloorShape shape,int q,EnumFacing side,int joins) } public static List parts(FloorShape shape,boolean alongX,int joins,EnumFacing bracket,boolean fullReach) { List result=new ArrayList<>(); + // Close the wall head in this floor cell, without filling the air under the jetty. + int supports=(joins>>16)&15&shape.mask; + for(int q=0;q<4;q++) if((supports&(1< parts(FloorShape shape,boolean alongX,int joins,EnumFac bracket==EnumFacing.WEST?EnumFacing.NORTH:EnumFacing.SOUTH; // Transform into block coordinates before extrusion, retaining a convex sloping web. double tangent=anchor[1],edge=anchor[0]; - double[][] points=new double[][]{{edge,6},{edge,8},{edge+reach,2},{edge+reach,0},{edge+reach-2,0},{edge,6}}; + // Counter-clockwise outline: the extrusion's caps and edges must face outwards. + // Do not repeat the first point; that would add a zero-area closing edge. + double[][] points={{edge,6},{edge+reach-2,0},{edge+reach,0},{edge+reach,2},{edge,8}}; // local web x increases inward from the edge; localFacing places it on the chosen side. result.add(new Part(points,tangent-1.5,tangent+1.5,Grain.FALLING,local)); } diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/LeadedPaneItem.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/LeadedPaneItem.java new file mode 100644 index 0000000..7680c12 --- /dev/null +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/LeadedPaneItem.java @@ -0,0 +1,15 @@ +package zone.moddev.mc.ironagearchitecture.block; + +import java.util.List; +import net.minecraft.creativetab.CreativeTabs; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.*; +import zone.moddev.mc.ironagearchitecture.EraTabs; + +public final class LeadedPaneItem extends Item { + public LeadedPaneItem(){setHasSubtypes(true);setMaxDamage(0);} + @Override public CreativeTabs[] getCreativeTabs(){return EraTabs.forItem(this);} + @Override public String getUnlocalizedName(ItemStack stack){return super.getUnlocalizedName()+"."+Math.floorMod(stack.getMetadata(),2);} + @Override public void getSubItems(Item item,CreativeTabs tab,List out){out.add(new ItemStack(item,1,0));out.add(new ItemStack(item,1,1));} + @Override public void addInformation(ItemStack stack,EntityPlayer player,List tooltip,boolean advanced){EraItemBlock.hint(tooltip,"pane.use");} +} diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/PartialWallBlock.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/PartialWallBlock.java index 3569d17..6a74a1c 100644 --- a/src/main/java/zone/moddev/mc/ironagearchitecture/block/PartialWallBlock.java +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/PartialWallBlock.java @@ -102,6 +102,13 @@ private static EnumFacing inverse(EnumFacing facing) { IBlockState previous=state;BlockPos previousPos=pos; for(int distance=1;distance<=16;distance++) { BlockPos at=pos.offset(direction,distance);IBlockState next=world.getBlockState(at); + if(next.getBlock() instanceof WindowBlock) { + WindowBlock window=(WindowBlock)next.getBlock(); + if(wood!=null&&wood.equals(window.wood)&&WallRuns.touches(previous,previousPos,next,at,direction)) { + int joins=window.joins(next,world,at);if((joins&WindowGeometry.DOWN)==0) mask|=WallGeometry.bit(direction)<<4;if((joins&WindowGeometry.UP)==0) mask|=WallGeometry.bit(direction)<<8; + } + break; + } if(!(next.getBlock() instanceof FrameBlock)||!timber((FrameBlock)next.getBlock())||!WallRuns.touches(previous,previousPos,next,at,direction)) break; FrameBlock block=(FrameBlock)next.getBlock(); if(block.family==FrameFamily.RAIL) {mask|=WallGeometry.bit(direction)<<((next.getValue(PANEL)&1)==0?4:8);break;} @@ -109,6 +116,7 @@ private static EnumFacing inverse(EnumFacing facing) { previous=next;previousPos=at; } } + mask=RailJoins.bridge(state,world,pos,mask); if(family==FrameFamily.RAIL) for(int dy:new int[]{-1,1}) { IBlockState next=world.getBlockState(pos.up(dy)); if(!(next.getBlock() instanceof PartialWallBlock)) continue; diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/PartialWallGeometry.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/PartialWallGeometry.java index 80f64b3..d7ce309 100644 --- a/src/main/java/zone/moddev/mc/ironagearchitecture/block/PartialWallGeometry.java +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/PartialWallGeometry.java @@ -23,15 +23,19 @@ public static List parts(WallProfile profile,FrameFamily family,int panel, int bits=(mask>>(h==0?4:8))&15; if(bits==0) continue; double y=h==0?0:13; - WallGeometry.box(result,x,y,z+.01,profile.x1,y+3,z+1.25,Grain.EAST_WEST); - WallGeometry.box(result,x,y,z+6.75,profile.x1,y+3,z+7.99,Grain.EAST_WEST); - WallGeometry.box(result,x+.01,y,z,x+1.25,y+3,profile.z1,Grain.NORTH_SOUTH); - WallGeometry.box(result,x+6.75,y,z,x+7.99,y+3,profile.z1,Grain.NORTH_SOUTH); + if((bits&WallGeometry.bit(facing.rotateY()))!=0) { + WallGeometry.box(result,x,y,z+.125,profile.x1,y+3,z+1.25,Grain.EAST_WEST); + WallGeometry.box(result,x+8-inner,y,z+6.75,profile.x1,y+3,z+7.875,Grain.EAST_WEST); + } + if((bits&WallGeometry.bit(facing.getOpposite()))!=0) { + WallGeometry.box(result,x+.125,y,z,x+1.25,y+3,profile.z1,Grain.NORTH_SOUTH); + WallGeometry.box(result,x+6.75,y,z+8-inner,x+7.875,y+3,profile.z1,Grain.NORTH_SOUTH); + } } } else { WallGeometry.box(result,profile.x0,0,.75,profile.x1,16,7.25,Grain.DAUB); int sourcePanel=family==FrameFamily.TERMINAL?1:panel; - for(Part part:WallGeometry.parts(family,sourcePanel,facing,mask)) { + for(Part part:WallGeometry.rawParts(family,sourcePanel,facing,mask)) { if(part.grain==Grain.DAUB) continue; double[][] polygon=part.outline; // Principal/end posts belong to the short piece, not a severed half-post. @@ -48,6 +52,6 @@ public static List parts(WallProfile profile,FrameFamily family,int panel, if(back>front) result.add(new Part(polygon,front,back,part.grain,part.localFacing)); } } - return Collections.unmodifiableList(result); + return !profile.corner&&family!=FrameFamily.RAIL?WallGeometry.trimRails(result,facing,mask):Collections.unmodifiableList(result); } } diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/RailJoins.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/RailJoins.java new file mode 100644 index 0000000..563880f --- /dev/null +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/RailJoins.java @@ -0,0 +1,44 @@ +package zone.moddev.mc.ironagearchitecture.block; + +import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Blocks; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.*; + +/** Complete a source-to-anchor run without recursively asking neighbouring blocks for their joins. */ +final class RailJoins { + private RailJoins(){ } + static int bridge(IBlockState state,IBlockAccess world,BlockPos pos,int mask) { + FrameBlock owner=(FrameBlock)state.getBlock();if(owner.family==FrameFamily.INFILL||owner.wood==null) return mask; + int sources=mask; + for(int height=0;height<2;height++) for(EnumFacing direction:EnumFacing.HORIZONTALS) { + int bit=WallGeometry.bit(direction)<<(height==0?4:8),opposite=WallGeometry.bit(direction.getOpposite())<<(height==0?4:8); + if((sources&bit)==0&&(sources&opposite)!=0&&anchor(world,state,pos,direction,height,owner.wood)) mask|=bit; + } + return mask; + } + private static IBlockState state(IBlockAccess world,BlockPos at){return world instanceof World&&!((World)world).isBlockLoaded(at)?Blocks.AIR.getDefaultState():world.getBlockState(at);} + private static boolean touches(IBlockState a,BlockPos pa,IBlockState b,BlockPos pb,EnumFacing direction) { + for(WallRuns.Run x:WallRuns.of(a,pa)) for(WallRuns.Run y:WallRuns.of(b,pb)) + if(x.tangent.getAxis()==direction.getAxis()&&x.touches(y,direction)) return true; + return false; + } + private static boolean anchor(IBlockAccess world,IBlockState previous,BlockPos pos,EnumFacing direction,int height,String wood) { + BlockPos before=pos; + for(int distance=1;distance<=16;distance++) { + BlockPos at=pos.offset(direction,distance);IBlockState next=state(world,at); + if(!(next.getBlock() instanceof FrameBlock)||next.getBlock() instanceof JettyFloorBlock) return false; + FrameBlock block=(FrameBlock)next.getBlock(); + if(!wood.equals(block.wood)||block.family==FrameFamily.INFILL||!touches(previous,before,next,at,direction)) return false; + double bottom=height==0?0:13,top=bottom+3; + for(WallGeometry.Part part:block.parts(next,0)) { + if(part.grain==WallGeometry.Grain.DAUB) continue; + double min=16,max=0;for(double[] p:part.outline){min=Math.min(min,p[1]);max=Math.max(max,p[1]);} + if(Math.min(max,top)-Math.max(min,bottom)>1e-6) return true; + } + previous=next;before=at; + } + return false; + } +} diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/SurfaceGeometry.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/SurfaceGeometry.java new file mode 100644 index 0000000..c13111a --- /dev/null +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/SurfaceGeometry.java @@ -0,0 +1,131 @@ +package zone.moddev.mc.ironagearchitecture.block; + +import java.util.*; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.AxisAlignedBB; +import zone.moddev.mc.ironagearchitecture.block.WallGeometry.*; + +/** Remove buried faces and assign one owner to coplanar timber intersections. */ +public final class SurfaceGeometry { + private static final double EPS=1e-8; + private SurfaceGeometry() { } + public static final class Face { + public final Part part; + public final double[][] points; + Face(Part part,double[][] points) {this.part=part;this.points=points;} + } + private static boolean box(Part part) { + if(part.outline.length!=4) return false; + for(int i=0;i<4;i++) { + double[] a=part.outline[i],b=part.outline[(i+1)%4]; + if(Math.abs(a[0]-b[0])>EPS&&Math.abs(a[1]-b[1])>EPS) return false; + } + return true; + } + private static int priority(Part part) { + switch(part.grain) { + case BOARDS_X:case BOARDS_Z:return 5; + case VERTICAL:return 4; + case EAST_WEST:case NORTH_SOUTH:return 3; + case RISING:case FALLING:return 2; + default:return 0; + } + } + private static double[] point(Part part,int index,boolean back) {return new double[]{part.outline[index][0],part.outline[index][1],back?part.back:part.front};} + private static List raw(Part part) { + int n=part.outline.length;List faces=new ArrayList<>(); + if(n<3) return faces; + double[][] front=new double[n][],back=new double[n][]; + for(int i=0;iEPS) {for(int j=0;j<3;j++) n[j]/=length;return n;} + } + return new double[3]; + } + private static double[][] clean(List input) { + List result=new ArrayList<>(); + for(double[] point:input) if(result.isEmpty()||distance(point,result.get(result.size()-1))>EPS) result.add(point); + if(result.size()>1&&distance(result.get(0),result.get(result.size()-1)) out=new ArrayList<>();double[] previous=polygon[polygon.length-1]; + double before=(previous[axis]-edge)*(positive?1:-1); + boolean was=inside?before>=-EPS:before<-EPS; + for(double[] current:polygon) { + double after=(current[axis]-edge)*(positive?1:-1);boolean now=inside?after>=-EPS:after<-EPS; + if(was!=now) { + double t=(-EPS-before)/(after-before);double[] cut=new double[3]; + for(int i=0;i<3;i++) cut[i]=previous[i]+t*(current[i]-previous[i]);out.add(cut); + } + if(now) out.add(current); + previous=current;before=after;was=now; + } + return clean(out); + } + public static double area(double[][] polygon) { + if(polygon.length<3) return 0; + double[] sum=new double[3]; + for(int i=0;i subtract(double[][] face,double[] box,boolean ownerWins) { + for(int axis=0;axis<3;axis++) { + double min=Double.POSITIVE_INFINITY,max=Double.NEGATIVE_INFINITY; + for(double[] p:face) {min=Math.min(min,p[axis]);max=Math.max(max,p[axis]);} + if(maxbox[axis+3]+EPS) return Collections.singletonList(face); + } + double[] normal=normal(face); + if(ownerWins) for(int axis=0;axis<3;axis++) { + double edge=normal[axis]>0?box[axis+3]:box[axis]; + if(Math.abs(normal[axis])>.999&&Math.abs(face[0][axis]-edge) visible=new ArrayList<>();double[][] remaining=face; + for(int plane=0;plane<6&&remaining.length>=3;plane++) { + int axis=plane%3;boolean positive=plane<3;double edge=box[axis+(positive?0:3)]; + double[][] outside=clip(remaining,axis,edge,positive,false); + if(area(outside)>1e-6) visible.add(outside); + remaining=clip(remaining,axis,edge,positive,true); + } + return visible; + } + public static List faces(List parts,EnumFacing facing) { + List result=new ArrayList<>(); + for(int i=0;i faces=raw(owner); + for(int j=0;jpriority(cover)||priority(owner)==priority(cover)&&i next=new ArrayList<>(); + for(double[][] face:faces) if(area(face)>1e-6) next.addAll(subtract(face,bounds,wins)); + faces=next; + } + for(double[][] face:faces) if(area(face)>1e-6) result.add(new Face(owner,face)); + } + return result; + } +} diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/TimberCorners.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/TimberCorners.java new file mode 100644 index 0000000..9cc6dcb --- /dev/null +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/TimberCorners.java @@ -0,0 +1,74 @@ +package zone.moddev.mc.ironagearchitecture.block; + +import java.util.*; +import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Blocks; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.*; +import net.minecraftforge.common.property.IUnlistedProperty; +import zone.moddev.mc.ironagearchitecture.ArchitectureBlocks; +import zone.moddev.mc.ironagearchitecture.block.WallGeometry.*; + +/** Tiny in-cell continuations prevent diagonally adjoining beams from pinching to a point. */ +public final class TimberCorners { + public static final IUnlistedProperty PROPERTY=new IUnlistedProperty() { + @Override public String getName(){return "timber_corners";} + @Override public boolean isValid(Integer value){return value!=null&&value>=0&&value<4096;} + @Override public Class getType(){return Integer.class;} + @Override public String valueToString(Integer value){return value.toString();} + }; + private TimberCorners(){ } + private static IBlockState state(IBlockAccess world,BlockPos pos){return world instanceof World&&!((World)world).isBlockLoaded(pos)?Blocks.AIR.getDefaultState():world.getBlockState(pos);} + private static boolean samePlane(IBlockState a,BlockPos pa,IBlockState b,BlockPos pb) { + for(WallRuns.Run x:WallRuns.of(a,pa)) for(WallRuns.Run y:WallRuns.of(b,pb)) + if(x.tangent.getAxis()==y.tangent.getAxis()&&Math.abs(x.depth0-y.depth0)<1e-6&&Math.abs(x.depth1-y.depth1)<1e-6) return true; + return false; + } + private static String endpoint(IBlockState state,BlockPos pos,double[] corner,EnumFacing.Axis normal) { + if(!(state.getBlock() instanceof FrameBlock)||state.getBlock() instanceof JettyFloorBlock) return null; + FrameBlock block=(FrameBlock)state.getBlock(); + if(block.family!=FrameFamily.BRACE&&!(block.family==FrameFamily.POST&&(state.getValue(FrameBlock.PANEL)&1)!=0)) return null; + for(Part part:block.parts(state,0)) if(part.grain==Grain.RISING||part.grain==Grain.FALLING) for(double[] vertex:part.outline) { + double[] p=part.transform(vertex[0],vertex[1],part.front,state.getValue(FrameBlock.FACING)); + double dx=Math.abs(pos.getX()*16+p[0]-corner[0]),dz=Math.abs(pos.getZ()*16+p[2]-corner[2]); + if(Math.abs(pos.getY()*16+p[1]-corner[1])<1e-6&&(normal==EnumFacing.Axis.X?dz<1e-6&&dx<=1.25:dx<1e-6&&dz<=1.25)) return block.wood; + } + return null; + } + public static int derive(IBlockState owner,IBlockAccess world,BlockPos pos) { + FrameBlock block=(FrameBlock)owner.getBlock();if(block instanceof JettyFloorBlock) return 0; + if(block instanceof PartialWallBlock&&((PartialWallBlock)block).profile.corner) return 0; + if(!(block instanceof PartialWallBlock)&&block.family==FrameFamily.TERMINAL&&(owner.getValue(FrameBlock.PANEL)&1)==0) return 0; + EnumFacing facing=owner.getValue(FrameBlock.FACING);int result=0; + for(int corner=0;corner<4;corner++) { + boolean right=(corner&1)!=0,top=(corner&2)!=0; + if(block instanceof PartialWallBlock) {WallProfile p=((PartialWallBlock)block).profile;int x=right?16:0;if(xp.x1) continue;} + BlockPos hp=pos.offset(right?facing.rotateY():facing.rotateYCCW()),vp=top?pos.up():pos.down(); + IBlockState h=state(world,hp),v=state(world,vp); + if(!samePlane(owner,pos,h,hp)||!samePlane(owner,pos,v,vp)) continue; + // Check the endpoints on either elevation; the wood is the same on both faces. + double[] local=WallGeometry.rotate(right?16:0,top?16:0,0,facing); + double[] point={pos.getX()*16+local[0],pos.getY()*16+local[1],pos.getZ()*16+local[2]}; + String wood=endpoint(h,hp,point,facing.getAxis()),other=endpoint(v,vp,point,facing.getAxis()); + if(wood==null||!wood.equals(other)||block.wood!=null&&!block.wood.equals(wood)) continue; + for(int w=0;w add(List base,boolean thin,int encoded) { + if(encoded==0) return base;List result=new ArrayList<>(base);double depth=thin?8:16,w=1.25; + for(int corner=0;corner<4;corner++) { + int wood=(encoded>>(corner*3)&7)-1;if(wood<0||wood>=ArchitectureBlocks.WOODS.length) continue; + boolean right=(corner&1)!=0,top=(corner&2)!=0; + double x=right?16:0,y=top?16:0; + double[][] triangle={{x,y},{x+(right?-w:w),y},{x,y+(top?-w:w)}}; + // Reflecting just one axis reverses winding. + if(right!=top) {double[] swap=triangle[1];triangle[1]=triangle[2];triangle[2]=swap;} + Grain grain=right!=top?Grain.RISING:Grain.FALLING; + result.add(new Part(triangle,.375,1.25,grain,EnumFacing.NORTH,false,ArchitectureBlocks.WOODS[wood])); + result.add(new Part(triangle,depth-1.25,depth-.375,grain,EnumFacing.NORTH,false,ArchitectureBlocks.WOODS[wood])); + } + return Collections.unmodifiableList(result); + } +} diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/WallGeometry.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/WallGeometry.java index a4aa9c1..f222119 100644 --- a/src/main/java/zone/moddev/mc/ironagearchitecture/block/WallGeometry.java +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/WallGeometry.java @@ -18,6 +18,7 @@ public static final class Part { public final Grain grain; public final EnumFacing localFacing; public final boolean horizontal; + public final String timber; public Part(double[][] outline,double front,double back,Grain grain) { this(outline,front,back,grain,EnumFacing.NORTH); } @@ -25,13 +26,22 @@ public Part(double[][] outline,double front,double back,Grain grain,EnumFacing l this(outline,front,back,grain,localFacing,false); } public Part(double[][] outline,double front,double back,Grain grain,EnumFacing localFacing,boolean horizontal) { + this(outline,front,back,grain,localFacing,horizontal,null); + } + public Part(double[][] outline,double front,double back,Grain grain,EnumFacing localFacing,boolean horizontal,String timber) { this.outline=outline;this.front=front;this.back=back;this.grain=grain; - this.localFacing=localFacing;this.horizontal=horizontal; + this.localFacing=localFacing;this.horizontal=horizontal;this.timber=timber; } public double[] transform(double x,double y,double z,EnumFacing facing) { double[] local=horizontal?rotate(x,z,y,localFacing):rotate(x,y,z,localFacing); return rotate(local[0],local[1],local[2],facing); } + public double[] inverse(double x,double y,double z,EnumFacing facing) { + double[] world=rotate(x,y,z,reverse(facing)); + double[] local=rotate(world[0],world[1],world[2],reverse(localFacing)); + return horizontal?new double[]{local[0],local[2],local[1]}:local; + } + private static EnumFacing reverse(EnumFacing facing) {return facing==EnumFacing.EAST?EnumFacing.WEST:facing==EnumFacing.WEST?EnumFacing.EAST:facing;} public AxisAlignedBB bounds(EnumFacing facing) { double minX=16,minY=16,minZ=16,maxX=0,maxY=0,maxZ=0; for(double[] p:outline) for(double z:new double[]{front,back}) { @@ -92,7 +102,7 @@ private static void diagonal(List parts,boolean rising,double x0,double x1 Grain grain=rising?Grain.RISING:Grain.FALLING; polygon=clip(clip(polygon,x0,true),x1,false); // Braces sit a fraction behind posts/rails, avoiding coplanar overlapping timber faces. - double inset=rising?.035:.045; + double inset=rising?.25:.375; parts.add(new Part(polygon,inset,DEPTH,grain,localFacing)); parts.add(new Part(polygon,16-DEPTH,16-inset,grain,localFacing)); } @@ -100,6 +110,11 @@ private static void diagonal(List parts,boolean rising,double x0,double x1 private static boolean requested(int mask,int shift,EnumFacing worldDirection) { return (mask&(bit(worldDirection)< parts(FrameFamily family,int panel,EnumFacing facing,int mask) { + List result=rawParts(family,panel,facing,mask); + return family==FrameFamily.RAIL||family==FrameFamily.INFILL||family==FrameFamily.TERMINAL&&(panel&1)==0?result:trimRails(result,facing,mask); + } + /** Partial walls trim only after their posts have moved and their outlines have been cut. */ + static List rawParts(FrameFamily family,int panel,EnumFacing facing,int mask) { List result=new ArrayList<>(); boolean corner=family==FrameFamily.TERMINAL&&(panel&1)==0; if(corner) { @@ -155,26 +170,49 @@ public static List parts(FrameFamily family,int panel,EnumFacing facing,in double x0=left?0:8,x1=right?16:8; if(corner) { if(right) { - box(result,0,y0,.01,16,y1,DEPTH,Grain.EAST_WEST); - box(result,0,y0,16-DEPTH,16,y1,16-.01,Grain.EAST_WEST); + box(result,0,y0,.125,16,y1,DEPTH,Grain.EAST_WEST); + box(result,16-POST,y0,16-DEPTH,16,y1,16-.125,Grain.EAST_WEST); } if(south) { - box(result,.01,y0,0,DEPTH,y1,16,Grain.NORTH_SOUTH); - box(result,16-DEPTH,y0,0,16-.01,y1,16,Grain.NORTH_SOUTH); + box(result,.125,y0,0,DEPTH,y1,16,Grain.NORTH_SOUTH); + box(result,16-DEPTH,y0,16-POST,16-.125,y1,16,Grain.NORTH_SOUTH); } } else { // Braces/studs are intersections within a rail run; principal posts can terminate one. if((family==FrameFamily.BRACE||family==FrameFamily.STUD||(family==FrameFamily.POST&&(panel&1)!=0))&&(left||right)) {x0=0;x1=16;} if(family==FrameFamily.TERMINAL&&right) x0=0; - box(result,x0,y0,.01,x1,y1,DEPTH,Grain.EAST_WEST); - box(result,x0,y0,16-DEPTH,x1,y1,16-.01,Grain.EAST_WEST); + box(result,x0,y0,.125,x1,y1,DEPTH,Grain.EAST_WEST); + box(result,x0,y0,16-DEPTH,x1,y1,16-.125,Grain.EAST_WEST); if(north||south) { double z0=north?0:8,z1=south?16:8; - box(result,.01,y0,z0,DEPTH,y1,z1,Grain.NORTH_SOUTH); - box(result,16-DEPTH,y0,z0,16-.01,y1,z1,Grain.NORTH_SOUTH); + box(result,.125,y0,z0,DEPTH,y1,z1,Grain.NORTH_SOUTH); + box(result,16-DEPTH,y0,z0,16-.125,y1,z1,Grain.NORTH_SOUTH); } } } return Collections.unmodifiableList(result); } + static List trimRails(List source,EnumFacing facing,int mask) { + List result=new ArrayList<>(); + for(Part rail:source) { + if(rail.grain!=Grain.EAST_WEST||rail.localFacing!=EnumFacing.NORTH||rail.horizontal) {result.add(rail);continue;} + double bottom=16,top=0; + for(double[] p:rail.outline) {bottom=Math.min(bottom,p[1]);top=Math.max(top,p[1]);} + int shift=bottom<8?4:8; + boolean left=requested(mask,shift,facing.rotateYCCW()),right=requested(mask,shift,facing.rotateY()); + if(left==right) {result.add(rail);continue;} + double low=Double.POSITIVE_INFINITY,high=Double.NEGATIVE_INFINITY; + for(Part member:source) { + if(member.horizontal||member.localFacing!=EnumFacing.NORTH||member.grain!=Grain.VERTICAL&&member.grain!=Grain.RISING&&member.grain!=Grain.FALLING) continue; + double[][] transposed=new double[member.outline.length][]; + for(int i=0;ihigh) continue; + double[][] outline=clip(rail.outline,left?high:low,!left); + if(outline.length>=3) result.add(new Part(outline,rail.front,rail.back,rail.grain,rail.localFacing,rail.horizontal)); + } + return Collections.unmodifiableList(result); + } } diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/WallRuns.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/WallRuns.java index 089c0ba..1af827b 100644 --- a/src/main/java/zone/moddev/mc/ironagearchitecture/block/WallRuns.java +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/WallRuns.java @@ -37,6 +37,7 @@ private static void add(List list,BlockPos pos,EnumFacing facing,double x0, } public static List of(IBlockState state,BlockPos pos) { List list=new ArrayList<>(); + if(state.getBlock() instanceof WindowBlock) {WindowBlock window=(WindowBlock)state.getBlock();add(list,pos,state.getValue(WindowBlock.FACING),0,16,window.back?8:0,window.thin?(window.back?16:8):16);return list;} if(!(state.getBlock() instanceof FrameBlock)||state.getBlock() instanceof JettyFloorBlock) return list; EnumFacing facing=state.getValue(FrameBlock.FACING); if(state.getBlock() instanceof PartialWallBlock) { diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/WindowBlock.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/WindowBlock.java new file mode 100644 index 0000000..e26e6ef --- /dev/null +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/WindowBlock.java @@ -0,0 +1,74 @@ +package zone.moddev.mc.ironagearchitecture.block; + +import java.util.*; +import javax.annotation.Nullable; +import net.minecraft.block.*; +import net.minecraft.block.material.Material; +import net.minecraft.block.properties.*; +import net.minecraft.block.state.*; +import net.minecraft.entity.*; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.*; +import net.minecraft.util.*; +import net.minecraft.util.math.*; +import net.minecraft.world.*; +import net.minecraftforge.common.property.*; +import zone.moddev.mc.ironagearchitecture.ArchitectureBlocks; + +/** Facing, open and powered consume exactly four saved bits. Hinge/style belong to identity. */ +public final class WindowBlock extends BlockHorizontal { + public static final PropertyBool OPEN=PropertyBool.create("open"),POWERED=PropertyBool.create("powered"); + public final String wood;public final int finish;public final boolean thin,diamond,right,back; + public WindowBlock(String wood,int finish,boolean thin,boolean diamond,boolean right,boolean back) { + super(Material.WOOD);this.wood=wood;this.finish=finish;this.thin=thin;this.diamond=diamond;this.right=right;this.back=back; + setHardness(2);setResistance(4);setSoundType(SoundType.WOOD);setLightOpacity(0);useNeighborBrightness=true; + setDefaultState(blockState.getBaseState().withProperty(FACING,EnumFacing.NORTH).withProperty(OPEN,false).withProperty(POWERED,false)); + } + @Override protected BlockStateContainer createBlockState() {return new ExtendedBlockState(this,new IProperty[]{FACING,OPEN,POWERED},new IUnlistedProperty[]{WindowView.PROPERTY});} + @Override public IBlockState getStateFromMeta(int meta){return getDefaultState().withProperty(FACING,EnumFacing.getHorizontal(meta&3)).withProperty(OPEN,(meta&4)!=0).withProperty(POWERED,(meta&8)!=0);} + @Override public int getMetaFromState(IBlockState state){return state.getValue(FACING).getHorizontalIndex()|(state.getValue(OPEN)?4:0)|(state.getValue(POWERED)?8:0);} + public WindowBlock canonical(){return ArchitectureBlocks.findWindow(wood,finish,thin,diamond,false);} + public boolean matches(IBlockState a,IBlockState b,boolean vertical) { + if(!(b.getBlock() instanceof WindowBlock)) return false;WindowBlock other=(WindowBlock)b.getBlock(); + return wood.equals(other.wood)&&finish==other.finish&&thin==other.thin&&back==other.back&&a.getValue(FACING)==b.getValue(FACING) + &&(!vertical||diamond==other.diamond&&right==other.right); + } + public int joins(IBlockState state,IBlockAccess world,BlockPos pos) { + int result=0;if(matches(state,world.getBlockState(pos.up()),true)) result|=WindowGeometry.UP; + if(matches(state,world.getBlockState(pos.down()),true)) result|=WindowGeometry.DOWN; + EnumFacing facing=state.getValue(FACING); + if(matches(state,neighbor(world,pos.offset(facing.rotateYCCW())),false)) result|=WindowGeometry.LEFT; + if(matches(state,neighbor(world,pos.offset(facing.rotateY())),false)) result|=WindowGeometry.RIGHT; + return result; + } + private IBlockState neighbor(IBlockAccess world,BlockPos pos){return world instanceof World&&!((World)world).isBlockLoaded(pos)?net.minecraft.init.Blocks.AIR.getDefaultState():world.getBlockState(pos);} + @Override public IBlockState getExtendedState(IBlockState state,IBlockAccess world,BlockPos pos){return ((IExtendedBlockState)state).withProperty(WindowView.PROPERTY,new WindowView(state,joins(state,world,pos),false));} + public List boxes(IBlockState state,IBlockAccess world,BlockPos pos,boolean proxy){return WindowGeometry.boxes(thin,right,state.getValue(OPEN),state.getValue(FACING),joins(state,world,pos),proxy,back);} + @Override public boolean isOpaqueCube(IBlockState state){return false;} + @Override public boolean isFullCube(IBlockState state){return false;} + @Override public boolean isSideSolid(IBlockState state,IBlockAccess world,BlockPos pos,EnumFacing side){return false;} + @Override public boolean isPassable(IBlockAccess world,BlockPos pos){return world.getBlockState(pos).getValue(OPEN);} + @Override public boolean canRenderInLayer(IBlockState state,BlockRenderLayer layer){return layer==BlockRenderLayer.SOLID||layer==BlockRenderLayer.TRANSLUCENT;} + @Override public AxisAlignedBB getBoundingBox(IBlockState state,IBlockAccess world,BlockPos pos){return union(boxes(state,world,pos,false));} + static AxisAlignedBB union(List boxes){AxisAlignedBB result=null;for(AxisAlignedBB box:boxes) result=result==null?box:result.union(box);return result==null?NULL_AABB:result;} + @Override public void addCollisionBoxToList(IBlockState state,World world,BlockPos pos,AxisAlignedBB mask,List boxes,@Nullable Entity entity){for(AxisAlignedBB box:boxes(state,world,pos,false)) addCollisionBoxToList(pos,mask,boxes,box);} + @Override public RayTraceResult collisionRayTrace(IBlockState state,World world,BlockPos pos,Vec3d start,Vec3d end){return trace(pos,start,end,boxes(state,world,pos,false));} + RayTraceResult trace(BlockPos pos,Vec3d start,Vec3d end,List boxes) { + RayTraceResult nearest=null;double distance=Double.POSITIVE_INFINITY; + for(AxisAlignedBB box:boxes){RayTraceResult hit=rayTrace(pos,start,end,box);if(hit!=null&&start.squareDistanceTo(hit.hitVec) BUSY=ThreadLocal.withInitial(()->false); + private static final Map> ENTITY_RETRIES=new WeakHashMap<>(); + private WindowController() { } + public static boolean busy(){return BUSY.get();} + public static List column(World world,BlockPos pos) { + List result=new ArrayList<>();if(!world.isBlockLoaded(pos)) return result;IBlockState state=world.getBlockState(pos);if(!(state.getBlock() instanceof WindowBlock)) return result; + WindowBlock block=(WindowBlock)state.getBlock();BlockPos low=pos; + while(low.getY()>0&&block.matches(state,world.getBlockState(low.down()),true)) low=low.down(); + for(BlockPos at=low;at.getY() parts=column(world,pos);if(parts.isEmpty()) return false; + Boolean power=null; + if(world.isAreaLoaded(pos.add(-2,0,-2),pos.add(2,0,2))) {power=false;for(BlockPos part:parts) power|=world.isBlockPowered(part);} + // A successful manual override also acknowledges a previously blocked power edge. + return set(world,parts,!world.getBlockState(pos).getValue(WindowBlock.OPEN),power,player,true); + } + public static boolean set(World world,List parts,boolean open,Boolean powered,EntityPlayer player,boolean sound) { + if(parts.isEmpty()||world.isRemote||busy()) return false; + Map before=new LinkedHashMap<>();boolean changed=false; + for(BlockPos pos:parts) { + IBlockState state=world.getBlockState(pos);if(!(state.getBlock() instanceof WindowBlock)) return false; + WindowBlock block=(WindowBlock)state.getBlock();BlockPos leaf=pos.offset(state.getValue(WindowBlock.FACING)); + if(!world.isBlockLoaded(leaf)){message(player,"blocked");return false;} + if(open) { + if(!world.isAirBlock(leaf)&&!owns(world,leaf,pos)){message(player,"blocked");return false;} + if(!state.getValue(WindowBlock.OPEN)&&!world.checkNoEntityCollision(WindowGeometry.sweep(block.right,state.getValue(WindowBlock.FACING),block.back).offset(pos))){ + message(player,"blocked");return false; + } + } + before.put(pos,state);before.put(leaf,world.getBlockState(leaf));changed|=state.getValue(WindowBlock.OPEN)!=open; + } + boolean ok=true;BUSY.set(true); + try { + for(BlockPos pos:parts) { + IBlockState old=before.get(pos);BlockPos leaf=pos.offset(old.getValue(WindowBlock.FACING)); + if(open) ok&=write(world,leaf,ArchitectureBlocks.WINDOW_LEAF.getDefaultState().withProperty(WindowLeafBlock.FACING,old.getValue(WindowBlock.FACING).getOpposite())); + else if(owns(world,leaf,pos)) ok&=write(world,leaf,Blocks.AIR.getDefaultState()); + IBlockState next=old.withProperty(WindowBlock.OPEN,open);if(powered!=null) next=next.withProperty(WindowBlock.POWERED,powered); + ok&=write(world,pos,next); + if(!ok) break; + } + if(!ok) for(Map.Entry entry:before.entrySet()) write(world,entry.getKey(),entry.getValue()); + } finally {BUSY.set(false);} + notify(world,before.keySet()); + if(ok&&sound&&changed) world.playSound(null,parts.get(0),open?SoundEvents.BLOCK_WOODEN_DOOR_OPEN:SoundEvents.BLOCK_WOODEN_DOOR_CLOSE,SoundCategory.BLOCKS,1,1); + return ok; + } + private static boolean write(World world,BlockPos pos,IBlockState state){return world.getBlockState(pos).equals(state)||world.setBlockState(pos,state,2);} + private static void notify(World world,Collection positions){for(BlockPos pos:positions){for(EnumFacing side:EnumFacing.values()){BlockPos next=pos.offset(side);if(world.isBlockLoaded(next)) world.getBlockState(next).neighborChanged(world,next,world.getBlockState(pos).getBlock());}world.markBlockRangeForRenderUpdate(pos.add(-1,-1,-1),pos.add(1,1,1));}} + public static void removeLeaf(World world,BlockPos pos,IBlockState state) { + if(world.isRemote||!(state.getBlock() instanceof WindowBlock)) return; + BlockPos leaf=pos.offset(state.getValue(WindowBlock.FACING));if(!world.isBlockLoaded(leaf)||!owns(world,leaf,pos)) return; + boolean previous=busy();BUSY.set(true);try{world.setBlockState(leaf,Blocks.AIR.getDefaultState(),2);}finally{BUSY.set(previous);} + } + public static void neighbor(World world,BlockPos pos) { + if(busy()||world.isRemote) return;List parts=column(world,pos);if(parts.isEmpty()) return; + if(!world.isAreaLoaded(pos.add(-2,0,-2),pos.add(2,0,2))) return; + IBlockState first=world.getBlockState(parts.get(0));boolean power=false; + for(BlockPos part:parts) power|=world.isBlockPowered(part); + if(power!=first.getValue(WindowBlock.POWERED)) { + if(!set(world,parts,power,power,null,true)&&power) rememberEntityBlocker(world,parts); + return; + } + repair(world,pos); + } + private static void rememberEntityBlocker(World world,List parts) { + // Block removal already sends an update; only moving entities need this retry. + for(BlockPos pos:parts) {IBlockState state=world.getBlockState(pos);BlockPos leaf=pos.offset(state.getValue(WindowBlock.FACING));if(!world.isBlockLoaded(leaf)||!world.isAirBlock(leaf)&&!owns(world,leaf,pos)) return;} + for(BlockPos pos:parts) { + IBlockState state=world.getBlockState(pos);WindowBlock block=(WindowBlock)state.getBlock(); + if(!world.checkNoEntityCollision(WindowGeometry.sweep(block.right,state.getValue(WindowBlock.FACING),block.back).offset(pos))) {ENTITY_RETRIES.computeIfAbsent(world,key->new HashSet<>()).add(parts.get(0).toImmutable());return;} + } + } + /** No idle window scan: only failed, entity-blocked power edges enter this short-lived queue. */ + public static void retryEntities(World world) { + Set waiting=ENTITY_RETRIES.remove(world);if(waiting==null||world.isRemote) return; + for(BlockPos pos:waiting) if(world.isBlockLoaded(pos)&&world.getBlockState(pos).getBlock() instanceof WindowBlock) neighbor(world,pos); + } + public static void repair(World world,BlockPos pos) { + if(busy()||world.isRemote) return;IBlockState state=world.getBlockState(pos); + if(state.getBlock() instanceof WindowLeafBlock) { + BlockPos owner=pos.offset(state.getValue(WindowLeafBlock.FACING));if(!world.isBlockLoaded(owner)) return; + IBlockState parent=world.getBlockState(owner); + if(!(parent.getBlock() instanceof WindowBlock)||!parent.getValue(WindowBlock.OPEN)||!owner.offset(parent.getValue(WindowBlock.FACING)).equals(pos)) { + BUSY.set(true);try{world.setBlockState(pos,Blocks.AIR.getDefaultState(),2);}finally{BUSY.set(false);} + } else repair(world,owner); + } else if(state.getBlock() instanceof WindowBlock&&state.getValue(WindowBlock.OPEN)) { + BlockPos leaf=pos.offset(state.getValue(WindowBlock.FACING));if(!world.isBlockLoaded(leaf)||owns(world,leaf,pos)) return; + List parts=column(world,pos); + if(!set(world,parts,true,null,null,false)) set(world,parts,false,null,null,false); + } + } + static boolean place(World world,BlockPos pos,IBlockState state,EntityPlayer player) { + WindowBlock block=(WindowBlock)state.getBlock();Boolean open=null,power=null; + for(BlockPos other:new BlockPos[]{pos.down(),pos.up()}) { + IBlockState neighbor=world.getBlockState(other);if(!block.matches(state,neighbor,true)) continue; + if(open!=null&&open!=neighbor.getValue(WindowBlock.OPEN)){message(player,"join");return false;} + open=neighbor.getValue(WindowBlock.OPEN);power=neighbor.getValue(WindowBlock.POWERED); + } + if(open!=null) state=state.withProperty(WindowBlock.OPEN,open).withProperty(WindowBlock.POWERED,power); + BlockPos leaf=pos.offset(state.getValue(WindowBlock.FACING)); + if(state.getValue(WindowBlock.OPEN)&&(!world.isBlockLoaded(leaf)||!world.isAirBlock(leaf)||!world.checkNoEntityCollision(WindowGeometry.sweep(block.right,state.getValue(WindowBlock.FACING),block.back).offset(pos)))){message(player,"blocked");return false;} + for(net.minecraft.util.math.AxisAlignedBB box:WindowGeometry.boxes(block.thin,block.right,state.getValue(WindowBlock.OPEN),state.getValue(WindowBlock.FACING),0,false,block.back)) if(!world.checkNoEntityCollision(box.offset(pos))) return false; + IBlockState previous=world.getBlockState(pos);boolean ok;BUSY.set(true); + try {ok=write(world,pos,state);if(ok&&state.getValue(WindowBlock.OPEN)) ok=write(world,leaf,ArchitectureBlocks.WINDOW_LEAF.getDefaultState().withProperty(WindowLeafBlock.FACING,state.getValue(WindowBlock.FACING).getOpposite()));if(!ok) write(world,pos,previous);} + finally{BUSY.set(false);} + if(ok){notify(world,Arrays.asList(pos,leaf));neighbor(world,pos);}return ok; + } +} diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/WindowEvents.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/WindowEvents.java new file mode 100644 index 0000000..eac9a41 --- /dev/null +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/WindowEvents.java @@ -0,0 +1,41 @@ +package zone.moddev.mc.ironagearchitecture.block; + +import net.minecraft.block.Block; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.WorldServer; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.storage.ExtendedBlockStorage; +import net.minecraftforge.event.world.ChunkEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +/** Only nonempty border sections need cross-chunk ownership repair; never polls or force-loads. */ +public final class WindowEvents { + @SubscribeEvent public void tick(net.minecraftforge.fml.common.gameevent.TickEvent.WorldTickEvent event) { + if(event.phase==net.minecraftforge.fml.common.gameevent.TickEvent.Phase.END&&!event.world.isRemote&&event.world.getTotalWorldTime()%10==0) WindowController.retryEntities(event.world); + } + @SubscribeEvent public void loaded(ChunkEvent.Load event) { + if(!(event.getWorld() instanceof WorldServer)||zone.moddev.mc.ironagearchitecture.ArchitectureBlocks.WINDOW_LEAF==null) return; + WorldServer world=(WorldServer)event.getWorld();Chunk chunk=event.getChunk(); + world.addScheduledTask(()->{ + if(!world.isBlockLoaded(new BlockPos(chunk.xPosition*16,64,chunk.zPosition*16))) return; + scan(world,chunk); + for(int dx=-1;dx<=1;dx++) for(int dz=-1;dz<=1;dz++) { + if(dx==0&&dz==0) continue; + int x=chunk.xPosition+dx,z=chunk.zPosition+dz;BlockPos pos=new BlockPos(x*16,64,z*16); + if(world.isBlockLoaded(pos)) scan(world,world.getChunkFromBlockCoords(pos)); + } + }); + } + private void scan(WorldServer world,Chunk chunk) { + ExtendedBlockStorage[] sections=chunk.getBlockStorageArray(); + for(int s=0;s out,double x,double y,double z,double xx,double yy,double zz,Grain grain) {WallGeometry.box(out,x,y,z,xx,yy,zz,grain);} + private static List fixed(boolean thin,int joins) { + List result=new ArrayList<>();double depth=thin?8:16; + for(double x:new double[]{0,14.5}) { + box(result,x,0,0,x+1.5,16,3,Grain.VERTICAL); + box(result,x,0,3,x+1.5,16,depth-1,Grain.DAUB); + box(result,x,0,depth-1,x+1.5,16,depth,Grain.VERTICAL); + } + if((joins&DOWN)==0) box(result,1.5,0,0,14.5,1.5,depth,Grain.EAST_WEST); + if((joins&UP)==0) box(result,1.5,14.5,0,14.5,16,depth,Grain.EAST_WEST); + return result; + } + private static List sash(int joins) { + List result=new ArrayList<>();double b=(joins&DOWN)!=0?0:1.5,t=(joins&UP)!=0?16:14.5; + box(result,1.5,b,1.7,2.25,t,2.4,Grain.VERTICAL); + box(result,13.75,b,1.7,14.5,t,2.4,Grain.VERTICAL); + if((joins&DOWN)==0) box(result,2.25,b,1.7,13.75,b+.75,2.4,Grain.EAST_WEST); + if((joins&UP)==0) box(result,2.25,t-.75,1.7,13.75,t,2.4,Grain.EAST_WEST); + return result; + } + private static double[] move(double[] p,boolean open,boolean right,EnumFacing facing,boolean back) { + double x=p[0],z=p[2]; + if(open) {double hinge=right?14.5:1.5;x=hinge+(right?-1:1)*(p[2]-2);z=2+(right?1:-1)*(p[0]-hinge);} + return WallGeometry.rotate(x,p[1],z+(back?8:0),facing); + } + private static double[][] clip(double[][] input,int axis,double edge,boolean greater) { + if(input.length==0) return input; + List out=new ArrayList<>();double[] previous=input[input.length-1]; + double before=(previous[axis]-edge)*(greater?1:-1); + for(double[] current:input) { + double after=(current[axis]-edge)*(greater?1:-1); + boolean a=before>=-1e-9,b=after>=-1e-9; + if(a!=b) {double t=Math.max(0,Math.min(1,-before/(after-before)));double[] q=new double[current.length];for(int i=0;i clean=new ArrayList<>(); + for(double[] p:out) if(clean.isEmpty()||distance(p,clean.get(clean.size()-1))>1e-8) clean.add(p); + if(clean.size()>1&&distance(clean.get(0),clean.get(clean.size()-1))<1e-8) clean.remove(clean.size()-1); + return clean.toArray(new double[clean.size()][]); + } + private static double distance(double[] a,double[] b) {double result=0;for(int i=0;i out,List parts,Material material,boolean moving,boolean open,boolean right,EnumFacing facing,boolean proxy,boolean back) { + for(SurfaceGeometry.Face face:SurfaceGeometry.faces(parts,EnumFacing.NORTH)) { + double[][] polygon=new double[face.points.length][]; + for(int i=0;i1e-7) out.add(new Face(material==Material.WOOD&&face.part.grain==Grain.DAUB?Material.DAUB:material,polygon)); + } + } + private static List glass(int joins,boolean pane) { + List result=new ArrayList<>();double b=pane?1:(joins&DOWN)!=0?0:2.25,t=pane?15:(joins&UP)!=0?16:13.75; + box(result,pane?1:2.25,b,2.02,pane?15:13.75,t,2.10,Grain.DAUB);return result; + } + private static List leading(boolean diamond,int joins,boolean pane) { + List result=new ArrayList<>();double low=pane?1:2.25,high=pane?15:13.75,b=pane?1:(joins&DOWN)!=0?0:2.25,t=pane?15:(joins&UP)!=0?16:13.75; + for(int orientation=0;orientation<2;orientation++) for(int line=-32;line<=48;line+=4) { + double[][] poly; + if(diamond) { + double sign=orientation==0?1:-1; + poly=new double[][]{{-32,sign*-32+line-.15},{48,sign*48+line-.15},{48,sign*48+line+.15},{-32,sign*-32+line+.15}}; + } else if(orientation==0) poly=new double[][]{{line-.15,-32},{line+.15,-32},{line+.15,48},{line-.15,48}}; + else poly=new double[][]{{-32,line-.15},{48,line-.15},{48,line+.15},{-32,line+.15}}; + poly=clip(clip(clip(clip(poly,0,low,true),0,high,false),1,b,true),1,t,false); + if(poly.length>=3) for(boolean rear:new boolean[]{false,true}) { + double z=rear?2.10+orientation*.04:1.90-orientation*.04; + result.add(new Part(poly,z,z+.07,Grain.DAUB)); + } + } + return result; + } + public static List faces(boolean thin,boolean diamond,boolean right,boolean open,EnumFacing facing,int joins,boolean proxy,boolean back) { + List out=new ArrayList<>(); + if(!proxy) emit(out,fixed(thin,joins),Material.WOOD,false,open,right,facing,false,back); + emit(out,sash(joins),Material.WOOD,true,open,right,facing,proxy,back); + emit(out,glass(joins,false),Material.GLASS,true,open,right,facing,proxy,back); + emit(out,leading(diamond,joins,false),Material.LEAD,true,open,right,facing,proxy,back); + return out; + } + public static List faces(boolean thin,boolean diamond,boolean right,boolean open,EnumFacing facing,int joins,boolean proxy) {return faces(thin,diamond,right,open,facing,joins,proxy,false);} + public static List boxes(boolean thin,boolean right,boolean open,EnumFacing facing,int joins,boolean proxy) {return boxes(thin,right,open,facing,joins,proxy,false);} + public static AxisAlignedBB sweep(boolean right,EnumFacing facing) {return sweep(right,facing,false);} + public static List paneFaces(boolean diamond) { + List out=new ArrayList<>();emit(out,glass(0,true),Material.GLASS,false,false,false,EnumFacing.NORTH,false,false); + emit(out,leading(diamond,0,true),Material.LEAD,false,false,false,EnumFacing.NORTH,false,false);return out; + } + private static AxisAlignedBB transform(AxisAlignedBB box,boolean open,boolean right,EnumFacing facing,boolean back) { + double[] bounds={Double.POSITIVE_INFINITY,Double.POSITIVE_INFINITY,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY,Double.NEGATIVE_INFINITY,Double.NEGATIVE_INFINITY}; + for(double x:new double[]{box.minX,box.maxX}) for(double y:new double[]{box.minY,box.maxY}) for(double z:new double[]{box.minZ,box.maxZ}) { + double[] p=move(new double[]{x*16,y*16,z*16},open,right,facing,back);for(int i=0;i<3;i++) {bounds[i]=Math.min(bounds[i],p[i]/16);bounds[i+3]=Math.max(bounds[i+3],p[i]/16);} + } + return new AxisAlignedBB(bounds[0],bounds[1],bounds[2],bounds[3],bounds[4],bounds[5]); + } + public static List boxes(boolean thin,boolean right,boolean open,EnumFacing facing,int joins,boolean proxy,boolean back) { + List result=new ArrayList<>(); + if(!proxy) for(Part part:fixed(thin,joins)) result.add(transform(part.bounds(EnumFacing.NORTH),false,right,facing,back)); + double b=(joins&DOWN)!=0?0:1.5,t=(joins&UP)!=0?16:14.5; + AxisAlignedBB leaf=transform(new AxisAlignedBB(1.5/16,b/16,1.7/16,14.5/16,t/16,2.4/16),open,right,facing,back); + if(proxy) leaf=leaf.offset(-facing.getFrontOffsetX(),0,-facing.getFrontOffsetZ()); + double x=Math.max(0,leaf.minX),y=Math.max(0,leaf.minY),z=Math.max(0,leaf.minZ),xx=Math.min(1,leaf.maxX),yy=Math.min(1,leaf.maxY),zz=Math.min(1,leaf.maxZ); + if(xx>x+1e-8&&yy>y+1e-8&&zz>z+1e-8) result.add(new AxisAlignedBB(x,y,z,xx,yy,zz)); + return result; + } + public static AxisAlignedBB sweep(boolean right,EnumFacing facing,boolean back) { + AxisAlignedBB closed=transform(new AxisAlignedBB(1.5/16,0,1.7/16,14.5/16,1,2.4/16),false,right,facing,back); + return closed.union(transform(new AxisAlignedBB(1.5/16,0,1.7/16,14.5/16,1,2.4/16),true,right,facing,back)); + } +} diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/WindowItem.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/WindowItem.java new file mode 100644 index 0000000..dd55439 --- /dev/null +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/WindowItem.java @@ -0,0 +1,44 @@ +package zone.moddev.mc.ironagearchitecture.block; + +import java.util.List; +import net.minecraft.block.state.IBlockState; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.*; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import zone.moddev.mc.ironagearchitecture.ArchitectureBlocks; + +public final class WindowItem extends EraItemBlock { + public WindowItem(WindowBlock block){super(block);} + @Override public EnumActionResult onItemUse(ItemStack stack,EntityPlayer player,World world,BlockPos pos,EnumHand hand,EnumFacing side,float x,float y,float z) { + if(!world.getBlockState(pos).getBlock().isReplaceable(world,pos)) pos=pos.offset(side); + if(stack.stackSize==0||!world.getBlockState(pos).getBlock().isReplaceable(world,pos)||!player.canPlayerEdit(pos,side,stack)) return EnumActionResult.FAIL; + WindowBlock base=(WindowBlock)block;EnumFacing facing=player.getHorizontalFacing().getOpposite(); + IBlockState previous=world.getBlockState(pos.offset(side.getOpposite()));Boolean right=null;Boolean back=null; + if(!player.isSneaking()) { + if(previous.getBlock() instanceof WindowBlock&&((WindowBlock)previous.getBlock()).thin==base.thin) {facing=previous.getValue(WindowBlock.FACING);right=((WindowBlock)previous.getBlock()).right;back=((WindowBlock)previous.getBlock()).back;} + else if(base.thin&&previous.getBlock() instanceof PartialWallBlock&&((PartialWallBlock)previous.getBlock()).profile==WallProfile.THIN) { + EnumFacing plane=previous.getValue(FrameBlock.FACING);BlockPos clicked=pos.offset(side.getOpposite()); + double[] centre=WallGeometry.rotate(8,0,4,plane); + facing=plane.getAxis()==EnumFacing.Axis.Z?(player.posZ=8; + } + for(BlockPos next:new BlockPos[]{pos.down(),pos.up()}) { + IBlockState neighbor=world.getBlockState(next); + if(neighbor.getBlock() instanceof WindowBlock) {WindowBlock other=(WindowBlock)neighbor.getBlock();if(base.wood.equals(other.wood)&&base.finish==other.finish&&base.thin==other.thin&&base.diamond==other.diamond){facing=neighbor.getValue(WindowBlock.FACING);right=other.right;back=other.back;break;}} + } + } + if(right==null) {EnumFacing inverse=facing==EnumFacing.EAST?EnumFacing.WEST:facing==EnumFacing.WEST?EnumFacing.EAST:facing;right=WallGeometry.rotate(x*16,0,z*16,inverse)[0]>=8;} + if(back==null) {EnumFacing inverse=facing==EnumFacing.EAST?EnumFacing.WEST:facing==EnumFacing.WEST?EnumFacing.EAST:facing;back=base.thin&&WallGeometry.rotate(x*16,0,z*16,inverse)[2]>=8;} + WindowBlock placed=ArchitectureBlocks.findWindow(base.wood,base.finish,base.thin,base.diamond,right,back); + if(world.isRemote) return EnumActionResult.SUCCESS; + if(!WindowController.place(world,pos,placed.getDefaultState().withProperty(WindowBlock.FACING,facing),player)) return EnumActionResult.FAIL; + world.playSound(null,pos,net.minecraft.init.SoundEvents.BLOCK_WOOD_PLACE,SoundCategory.BLOCKS,1,.8F); + if(!player.capabilities.isCreativeMode) stack.stackSize--;return EnumActionResult.SUCCESS; + } + @Override public void addInformation(ItemStack stack,EntityPlayer player,List tooltip,boolean advanced) { + hint(tooltip,"window.use");hint(tooltip,"window.place");hint(tooltip,"window.glaze");if(((WindowBlock)block).finish==0) hint(tooltip,"wash"); + } +} diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/WindowLeafBlock.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/WindowLeafBlock.java new file mode 100644 index 0000000..ce2437e --- /dev/null +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/WindowLeafBlock.java @@ -0,0 +1,52 @@ +package zone.moddev.mc.ironagearchitecture.block; + +import java.util.*; +import javax.annotation.Nullable; +import net.minecraft.block.*; +import net.minecraft.block.material.Material; +import net.minecraft.block.properties.IProperty; +import net.minecraft.block.state.*; +import net.minecraft.entity.*; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.*; +import net.minecraft.util.math.*; +import net.minecraft.world.*; +import net.minecraftforge.common.property.*; + +/** Outside-cell portion of an open sash. No ItemBlock, independent drops or tile entity. */ +public final class WindowLeafBlock extends BlockHorizontal { + public WindowLeafBlock(){super(Material.WOOD);setHardness(2);setLightOpacity(0);useNeighborBrightness=true;setDefaultState(blockState.getBaseState().withProperty(FACING,EnumFacing.NORTH));} + @Override protected BlockStateContainer createBlockState(){return new ExtendedBlockState(this,new IProperty[]{FACING},new IUnlistedProperty[]{WindowView.PROPERTY});} + @Override public IBlockState getStateFromMeta(int meta){return getDefaultState().withProperty(FACING,EnumFacing.getHorizontal(meta&3));} + @Override public int getMetaFromState(IBlockState state){return state.getValue(FACING).getHorizontalIndex();} + public BlockPos owner(IBlockState state,BlockPos pos){return pos.offset(state.getValue(FACING));} + public WindowView view(IBlockState state,IBlockAccess world,BlockPos pos) { + BlockPos owner=owner(state,pos);if(world instanceof World&&!((World)world).isBlockLoaded(owner)) return null;IBlockState parent=world.getBlockState(owner); + if(!(parent.getBlock() instanceof WindowBlock)||!parent.getValue(WindowBlock.OPEN)||!owner.offset(parent.getValue(WindowBlock.FACING)).equals(pos)) return null; + return new WindowView(parent,((WindowBlock)parent.getBlock()).joins(parent,world,owner),true); + } + @Override public IBlockState getExtendedState(IBlockState state,IBlockAccess world,BlockPos pos){return ((IExtendedBlockState)state).withProperty(WindowView.PROPERTY,view(state,world,pos));} + private List boxes(IBlockState state,IBlockAccess world,BlockPos pos){WindowView view=view(state,world,pos);if(view==null) return Collections.emptyList();WindowBlock block=(WindowBlock)view.state.getBlock();return WindowGeometry.boxes(block.thin,block.right,true,view.state.getValue(WindowBlock.FACING),view.joins,true,block.back);} + @Override public boolean isOpaqueCube(IBlockState state){return false;} + @Override public boolean isFullCube(IBlockState state){return false;} + @Override public boolean isSideSolid(IBlockState state,IBlockAccess world,BlockPos pos,EnumFacing side){return false;} + @Override public boolean canRenderInLayer(IBlockState state,BlockRenderLayer layer){return layer==BlockRenderLayer.SOLID||layer==BlockRenderLayer.TRANSLUCENT;} + @Override public AxisAlignedBB getBoundingBox(IBlockState state,IBlockAccess world,BlockPos pos){return WindowBlock.union(boxes(state,world,pos));} + @Override public void addCollisionBoxToList(IBlockState state,World world,BlockPos pos,AxisAlignedBB mask,List boxes,@Nullable Entity entity){for(AxisAlignedBB box:boxes(state,world,pos)) addCollisionBoxToList(pos,mask,boxes,box);} + @Override public RayTraceResult collisionRayTrace(IBlockState state,World world,BlockPos pos,Vec3d start,Vec3d end){WindowView view=view(state,world,pos);return view==null?null:((WindowBlock)view.state.getBlock()).trace(pos,start,end,boxes(state,world,pos));} + @Override public boolean onBlockActivated(World world,BlockPos pos,IBlockState state,EntityPlayer player,EnumHand hand,@Nullable ItemStack held,EnumFacing side,float x,float y,float z) { + WindowView view=view(state,world,pos);return view!=null&&((WindowBlock)view.state.getBlock()).onBlockActivated(world,owner(state,pos),view.state,player,hand,held,side,x,y,z); + } + @Override public boolean removedByPlayer(IBlockState state,World world,BlockPos pos,EntityPlayer player,boolean harvest) { + WindowView view=view(state,world,pos);BlockPos owner=owner(state,pos); + if(view!=null&&(!world.isBlockModifiable(player,owner)||!player.canPlayerEdit(owner,EnumFacing.UP,player.getHeldItemMainhand()))) return false; + if(!world.isRemote) {if(view!=null) world.destroyBlock(owner,!player.capabilities.isCreativeMode);else world.setBlockToAir(pos);}return true; + } + @Override public void harvestBlock(World world,EntityPlayer player,BlockPos pos,IBlockState state,@Nullable net.minecraft.tileentity.TileEntity tile,@Nullable ItemStack stack){ } + @Override public List getDrops(IBlockAccess world,BlockPos pos,IBlockState state,int fortune){return Collections.emptyList();} + @Override public ItemStack getPickBlock(IBlockState state,RayTraceResult hit,World world,BlockPos pos,EntityPlayer player){WindowView view=view(state,world,pos);return view==null?null:new ItemStack(((WindowBlock)view.state.getBlock()).canonical());} + @Override public void breakBlock(World world,BlockPos pos,IBlockState state){if(!world.isRemote&&!WindowController.busy()) {BlockPos owner=owner(state,pos);if(world.isBlockLoaded(owner)) WindowController.set(world,WindowController.column(world,owner),false,null,null,false);}super.breakBlock(world,pos,state);} + @Override public void neighborChanged(IBlockState state,World world,BlockPos pos,Block changed){WindowController.repair(world,pos);} + @Override public void onBlockExploded(World world,BlockPos pos,Explosion explosion){WindowView view=view(world.getBlockState(pos),world,pos);if(view!=null) world.destroyBlock(owner(world.getBlockState(pos),pos),true);else world.setBlockToAir(pos);} +} diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/block/WindowView.java b/src/main/java/zone/moddev/mc/ironagearchitecture/block/WindowView.java new file mode 100644 index 0000000..7411f59 --- /dev/null +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/block/WindowView.java @@ -0,0 +1,15 @@ +package zone.moddev.mc.ironagearchitecture.block; + +import net.minecraft.block.state.IBlockState; +import net.minecraftforge.common.property.IUnlistedProperty; + +/** Render-only snapshot; no world/client objects or saved data. */ +public final class WindowView { + public static final IUnlistedProperty PROPERTY=new IUnlistedProperty() { + public String getName(){return "window_view";} public boolean isValid(WindowView view){return true;} + public Class getType(){return WindowView.class;} public String valueToString(WindowView view){return String.valueOf(view);} + }; + public final IBlockState state;public final int joins;public final boolean proxy; + public WindowView(IBlockState state,int joins,boolean proxy){this.state=state;this.joins=joins;this.proxy=proxy;} + @Override public String toString(){return state+":"+joins+":"+proxy;} +} diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/client/ClientProxy.java b/src/main/java/zone/moddev/mc/ironagearchitecture/client/ClientProxy.java index 700edfd..f2360cd 100644 --- a/src/main/java/zone/moddev/mc/ironagearchitecture/client/ClientProxy.java +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/client/ClientProxy.java @@ -19,6 +19,7 @@ import zone.moddev.mc.ironagearchitecture.block.FrameBlock; import zone.moddev.mc.ironagearchitecture.block.RoofBlock; import zone.moddev.mc.ironagearchitecture.block.JettyBracketItem; +import zone.moddev.mc.ironagearchitecture.block.WindowBlock; import net.minecraftforge.event.world.WorldEvent; import net.minecraftforge.client.event.TextureStitchEvent; import net.minecraft.util.ResourceLocation; @@ -26,6 +27,7 @@ /** Models are registered before baking and never referenced by common classes. */ public final class ClientProxy extends CommonProxy { @Override public void preInit() { + super.preInit(); MinecraftForge.EVENT_BUS.register(this); for (Block block : ArchitectureBlocks.VISIBLE) { int count = block instanceof FrameBlock ? ((FrameBlock)block).variants() : 1; @@ -35,8 +37,18 @@ public final class ClientProxy extends CommonProxy { } } for(JettyBracketItem item:ArchitectureBlocks.BRACKETS) ModelLoader.setCustomModelResourceLocation(item,0,new ModelResourceLocation(item.getRegistryName(),"inventory")); + if(ArchitectureBlocks.LEADED_PANE!=null) for(int meta=0;meta<2;meta++) ModelLoader.setCustomModelResourceLocation(ArchitectureBlocks.LEADED_PANE,meta,new ModelResourceLocation("ironagearchitecture:leaded_pane_"+meta,"inventory")); } @SubscribeEvent public void bake(ModelBakeEvent event) { + for(WindowBlock window:ArchitectureBlocks.WINDOWS) { + java.util.Map shared=new java.util.HashMap<>(); + for(IBlockState state:window.getBlockState().getValidStates()) {ModelResourceLocation key=new ModelResourceLocation(window.getRegistryName(),variant(state));int shape=window.getMetaFromState(state)&7;WindowBakedModel model=shared.get(shape);if(model==null){model=new WindowBakedModel(event.getModelRegistry().getObject(key),state.withProperty(WindowBlock.POWERED,false));shared.put(shape,model);}event.getModelRegistry().putObject(key,model);} + if(!window.right&&!window.back) {ModelResourceLocation key=new ModelResourceLocation(window.getRegistryName(),"inventory");event.getModelRegistry().putObject(key,new WindowBakedModel(event.getModelRegistry().getObject(key),window.getDefaultState()));} + } + if(ArchitectureBlocks.WINDOW_LEAF!=null) { + for(IBlockState state:ArchitectureBlocks.WINDOW_LEAF.getBlockState().getValidStates()) {ModelResourceLocation key=new ModelResourceLocation(ArchitectureBlocks.WINDOW_LEAF.getRegistryName(),variant(state));event.getModelRegistry().putObject(key,new WindowBakedModel(event.getModelRegistry().getObject(key),(IBlockState)null));} + for(int meta=0;meta<2;meta++) {ModelResourceLocation key=new ModelResourceLocation("ironagearchitecture:leaded_pane_"+meta,"inventory");event.getModelRegistry().putObject(key,new WindowBakedModel(event.getModelRegistry().getObject(key),meta==0));} + } for(FrameBlock block:ArchitectureBlocks.meshes()) { for(IBlockState state:block.getBlockState().getValidStates()) { ModelResourceLocation key=new ModelResourceLocation(block.getRegistryName(),variant(state)); @@ -62,6 +74,7 @@ public final class ClientProxy extends CommonProxy { } } @SubscribeEvent public void stitch(TextureStitchEvent.Pre event) { + event.getMap().registerSprite(new ResourceLocation("minecraft:blocks/glass_silver"));event.getMap().registerSprite(new ResourceLocation("minecraft:blocks/iron_block")); for(String wood:ArchitectureBlocks.WOODS) { event.getMap().registerSprite(new ResourceLocation("ironagearchitecture:blocks/beam_"+wood)); event.getMap().registerSprite(new ResourceLocation("minecraft:blocks/log_"+(wood.equals("dark_oak")?"big_oak":wood)+"_top")); diff --git a/src/main/java/zone/moddev/mc/ironagearchitecture/client/WallBakedModel.java b/src/main/java/zone/moddev/mc/ironagearchitecture/client/WallBakedModel.java index 37fd732..d35787e 100644 --- a/src/main/java/zone/moddev/mc/ironagearchitecture/client/WallBakedModel.java +++ b/src/main/java/zone/moddev/mc/ironagearchitecture/client/WallBakedModel.java @@ -21,6 +21,8 @@ import net.minecraftforge.common.property.IExtendedBlockState; import zone.moddev.mc.ironagearchitecture.block.FrameBlock; import zone.moddev.mc.ironagearchitecture.block.JettyFloorBlock; +import zone.moddev.mc.ironagearchitecture.block.SurfaceGeometry; +import zone.moddev.mc.ironagearchitecture.block.TimberCorners; import zone.moddev.mc.ironagearchitecture.block.WallGeometry; import zone.moddev.mc.ironagearchitecture.block.WallGeometry.Grain; import zone.moddev.mc.ironagearchitecture.block.WallGeometry.Part; @@ -31,7 +33,7 @@ public final class WallBakedModel implements IBakedModel { private final FrameBlock block; private final IBlockState state; private final TextureAtlasSprite daub,wood,end,boards; - private final Map> cache=new ConcurrentHashMap<>(); + private final Map> cache=new ConcurrentHashMap<>(); public WallBakedModel(IBakedModel base,FrameBlock block,IBlockState state) { this.base=base;this.block=block;this.state=state; daub=sprite("ironagearchitecture:blocks/"+(block.finish(state.getValue(FrameBlock.PANEL))==0?"cob":"limewashed_cob")); @@ -43,31 +45,40 @@ public WallBakedModel(IBakedModel base,FrameBlock block,IBlockState state) { @Override public List getQuads(@Nullable IBlockState extended,@Nullable EnumFacing side,long seed) { if(side!=null) return Collections.emptyList(); Integer mask=extended instanceof IExtendedBlockState?((IExtendedBlockState)extended).getValue(FrameBlock.CONNECTIONS):null; - int key=mask==null?0:mask; + Integer patch=extended instanceof IExtendedBlockState?((IExtendedBlockState)extended).getValue(TimberCorners.PROPERTY):null; + int joins=mask==null?0:mask,corners=patch==null?0:patch;long key=(joins&0xffffffffL)|((long)corners<<32); List found=cache.get(key); if(found!=null) return found; List quads=new ArrayList<>(); - for(Part part:block.parts(state,key)) { - int n=part.outline.length; - if(n==4) { - add(quads,part,new double[][]{point(part,0,false),point(part,3,false),point(part,2,false),point(part,1,false)}); - add(quads,part,new double[][]{point(part,0,true),point(part,1,true),point(part,2,true),point(part,3,true)}); - } else for(int i=1;i128) cache.clear(); cache.put(key,found); return found; } - private static double[] point(Part part,int i,boolean back) { return new double[]{part.outline[i][0],part.outline[i][1],back?part.back:part.front}; } private void add(List quads,Part part,double[][] points) { + EnumFacing facing=state.getValue(BlockHorizontal.FACING); + double[][] positions=new double[4][3]; + for(int v=0;v<4;v++) { + double[] point=points[part.horizontal?3-v:v]; + double[] transformed=part.transform(point[0],point[1],point[2],facing); + for(int axis=0;axis<3;axis++) positions[v][axis]=(float)(transformed[axis]/16); + } + // Clipping can leave slivers narrower than a render vertex's float precision. + // Discard them after conversion, rather than emitting collapsed triangles at joins. + double area=0; + for(int i=1;i<3;i++) { + double[] a=positions[0],b=positions[i],c=positions[i+1]; + double x=(b[1]-a[1])*(c[2]-a[2])-(b[2]-a[2])*(c[1]-a[1]); + double y=(b[2]-a[2])*(c[0]-a[0])-(b[0]-a[0])*(c[2]-a[2]); + double z=(b[0]-a[0])*(c[1]-a[1])-(b[1]-a[1])*(c[0]-a[0]); + area+=x*x+y*y+z*z; + } + if(area<1e-18) return; double[] a=points[0],b=points[1],c=points[2]; double nx=(b[1]-a[1])*(c[2]-a[2])-(b[2]-a[2])*(c[1]-a[1]); double ny=(b[2]-a[2])*(c[0]-a[0])-(b[0]-a[0])*(c[2]-a[2]); @@ -79,15 +90,17 @@ private void add(List quads,Part part,double[][] points) { ||part.grain==Grain.EAST_WEST&&Math.abs(nx)>.9 ||part.grain==Grain.NORTH_SOUTH&&Math.abs(nz)>.9; TextureAtlasSprite texture=part.grain==Grain.DAUB?daub:part.grain==Grain.BOARDS_X||part.grain==Grain.BOARDS_Z?boards:alongEnd?end:wood; - EnumFacing facing=state.getValue(BlockHorizontal.FACING); + if(part.timber!=null) texture=sprite("ironagearchitecture:blocks/beam_"+part.timber); double[] origin=part.transform(0,0,0,facing),normalPoint=part.transform(nx,ny,nz,facing); float rx=(float)(normalPoint[0]-origin[0]),ry=(float)(normalPoint[1]-origin[1]),rz=(float)(normalPoint[2]-origin[2]); VertexFormat format=DefaultVertexFormats.ITEM; UnpackedBakedQuad.Builder builder=new UnpackedBakedQuad.Builder(format); builder.setTexture(texture);builder.setApplyDiffuseLighting(true); builder.setQuadOrientation(EnumFacing.getFacingFromVector(rx,ry,rz)); - for(double[] point:points) { - double[] p=part.transform(point[0],point[1],point[2],facing); + for(int vertex=0;vertex.9?point[0]:Math.abs(nx)>.9?point[2]:point[0];v=Math.abs(ny)>.9?point[2]:16-point[1];} else if(part.grain==Grain.RISING||part.grain==Grain.FALLING) { @@ -104,7 +117,7 @@ else if(part.grain==Grain.RISING||part.grain==Grain.FALLING) { // Keep edge faces inside their sprite, including at oblique mipmapped floor edges. float tu=texture.getInterpolatedU(.008+.999*Math.max(0,Math.min(16,u))),tv=texture.getInterpolatedV(.008+.999*Math.max(0,Math.min(16,v))); for(int element=0;element> cache=new HashMap<>(); + public WindowBakedModel(IBakedModel base,IBlockState state){this.base=base;fallback=state;pane=null;} + public WindowBakedModel(IBakedModel base,boolean diamond){this.base=base;fallback=null;pane=diamond;} + private TextureAtlasSprite sprite(String name){return Minecraft.getMinecraft().getTextureMapBlocks().getAtlasSprite(name);} + @Override public synchronized List getQuads(@Nullable IBlockState state,@Nullable EnumFacing side,long seed) { + if(side!=null) return Collections.emptyList();BlockRenderLayer layer=MinecraftForgeClient.getRenderLayer(); + WindowView view=state instanceof IExtendedBlockState?((IExtendedBlockState)state).getValue(WindowView.PROPERTY):null; + if(view==null&&fallback!=null&&fallback.getBlock() instanceof WindowBlock) view=new WindowView(fallback,0,false); + if(view==null&&pane==null) return Collections.emptyList(); + String key=(pane!=null?"pane"+pane:view.state.withProperty(WindowBlock.POWERED,false)+":"+view.joins+":"+view.proxy)+":"+layer;List found=cache.get(key);if(found!=null) return found; + WindowBlock block=view==null?null:(WindowBlock)view.state.getBlock(); + List faces=pane!=null?WindowGeometry.paneFaces(pane):WindowGeometry.faces(block.thin,block.diamond,block.right,view.state.getValue(WindowBlock.OPEN),view.state.getValue(WindowBlock.FACING),view.joins,view.proxy,block.back); + List quads=new ArrayList<>(); + for(WindowGeometry.Face face:faces) { + boolean glass=face.material==WindowGeometry.Material.GLASS; + if(layer!=null&&(glass?layer!=BlockRenderLayer.TRANSLUCENT:layer!=BlockRenderLayer.SOLID)) continue; + double[][] p=face.vertices; + if(p.length==4) add(quads,p,face.material,block); + else for(int i=1;i128) cache.clear();cache.put(key,found);return found; + } + private void add(List out,double[][] original,WindowGeometry.Material material,WindowBlock block) { + double[][] p=new double[4][3];for(int v=0;v<4;v++) for(int a=0;a<3;a++) p[v][a]=(float)(original[v][a]/16); + double nx=0,ny=0,nz=0,length=0; + for(int i=1;i<3&&length<1e-9;i++) {double[] a=p[0],b=p[i],c=p[i+1]; + nx=(b[1]-a[1])*(c[2]-a[2])-(b[2]-a[2])*(c[1]-a[1]);ny=(b[2]-a[2])*(c[0]-a[0])-(b[0]-a[0])*(c[2]-a[2]);nz=(b[0]-a[0])*(c[1]-a[1])-(b[1]-a[1])*(c[0]-a[0]);length=Math.sqrt(nx*nx+ny*ny+nz*nz); + } + if(length<1e-9) return;nx/=length;ny/=length;nz/=length; + String wood=block==null?"oak":block.wood; + TextureAtlasSprite texture=sprite(material==WindowGeometry.Material.GLASS?"minecraft:blocks/glass_silver":material==WindowGeometry.Material.LEAD?"minecraft:blocks/iron_block":material==WindowGeometry.Material.DAUB?"ironagearchitecture:blocks/"+(block.finish==0?"cob":"limewashed_cob"):"ironagearchitecture:blocks/beam_"+wood); + VertexFormat format=DefaultVertexFormats.ITEM;UnpackedBakedQuad.Builder builder=new UnpackedBakedQuad.Builder(format); + builder.setTexture(texture);builder.setApplyDiffuseLighting(material!=WindowGeometry.Material.GLASS);builder.setQuadOrientation(EnumFacing.getFacingFromVector((float)nx,(float)ny,(float)nz)); + float color=material==WindowGeometry.Material.LEAD?.22F:1; + for(int v=0;v<4;v++) for(int e=0;e savedBlocks=new ArrayList<>(ArchitectureBlocks.VISIBLE); + savedBlocks.removeIf(block->block instanceof WindowBlock); for(FrameBlock block:ArchitectureBlocks.meshes()) if(!savedBlocks.contains(block)) savedBlocks.add(block); + if(Boolean.getBoolean("iaa.legacyJetties")) { + int index=0; + for(Block block:savedBlocks) for(int meta=0;meta<16;meta++) { + require(world.getBlockState(new BlockPos(100+index,70,100)).equals(block.getStateFromMeta(meta)),"0.1.2 saved state "+index); + index++; + } + require(index==4448,"0.1.2 legacy matrix incomplete"); + } for(Block block:savedBlocks) { int count=block instanceof FrameBlock?((FrameBlock)block).variants():1; for(int meta=0;meta<16;meta++) { @@ -80,7 +90,7 @@ public final class ArchitectureProbe { states++; } } - require(variants==Integer.getInteger("iaa.expectedVariants",460),"Visible variants "+variants); + require(variants==Integer.getInteger("iaa.expectedVariants",ArchitectureConfig.windows?510:460),"Visible variants "+variants); for(IRecipe recipe:CraftingManager.getInstance().getRecipeList()) { ItemStack output=recipe.getRecipeOutput(); if(output==null||!"ironagearchitecture".equals(output.getItem().getRegistryName().getResourceDomain())) continue; @@ -99,13 +109,16 @@ public final class ArchitectureProbe { require(actual!=null&&ItemStack.areItemStacksEqual(actual,output),"Recipe shadowed "+output); recipes++; } - if(variants==460) { - require(recipes==1077,"Recipe catalog changed "+recipes); + if(variants>=460) { + require(recipes==EraProbe.expectedRecipes(),"Recipe catalog changed "+recipes); verifyRoofs(world); verifyWalls(world); verifyPlacementAndWalking(world); verifyJetties(world); - if(!reload) new JettyPreview().build(world); + verifyWallHeads(world); + HorizontalRailProbe.verify(world); + if(ArchitectureConfig.windows) WindowProbe.verify(world,reload); + if(!reload) {new JettyPreview().build(world);if(ArchitectureConfig.windows) WindowPreview.build(world);TimberJoinProbe.preview(world);} for(int finish=0;finish<2;finish++) { Block doubled=Block.getBlockFromName("ironagearchitecture:"+ArchitectureBlocks.FINISHES[finish]+"_double_slab"); List drops=doubled.getDrops(world,new BlockPos(90,70,100),doubled.getDefaultState(),0); @@ -119,24 +132,52 @@ public final class ArchitectureProbe { world.setSpawnPoint(new BlockPos(7,64,-9)); server.saveAllWorlds(false); String result="{\"passed\":true,\"variants\":"+variants+",\"recipes\":"+recipes+",\"states\":"+states+",\"reload\":"+reload - +",\"legacy\":"+Boolean.getBoolean("iaa.legacy")+",\"legacyDaub\":"+Boolean.getBoolean("iaa.legacyDaub")+",\"runtimeSha256\":\""+System.getProperty("iaa.runtimeSha256","development")+"\"}\n"; + +",\"legacy\":"+Boolean.getBoolean("iaa.legacy")+",\"legacyDaub\":"+Boolean.getBoolean("iaa.legacyDaub") + +",\"legacyJetties\":"+Boolean.getBoolean("iaa.legacyJetties")+",\"eras\":"+new com.google.gson.Gson().toJson(zone.moddev.mc.ironagearchitecture.ArchitectureConfig.survivalEras.allowed) + +",\"eraMatrix\":"+Boolean.getBoolean("iaa.eraMatrix")+",\"recipeMatrix\":"+Boolean.getBoolean("iaa.recipeMatrix")+",\"runtimeSha256\":\""+System.getProperty("iaa.runtimeSha256","development")+"\"}\n"; Files.write(Paths.get(System.getProperty("iaa.receipt","architecture-probe.json")),result.getBytes(StandardCharsets.UTF_8)); System.out.println("IAA_PROBE_PASS "+result.trim()); if(!Boolean.getBoolean("iaa.keepRunning")) server.initiateShutdown(); } private void verifyConfigMatrix() throws Exception { - boolean cob=ArchitectureConfig.cob,frames=ArchitectureConfig.frames,thatch=ArchitectureConfig.thatch,slate=ArchitectureConfig.slate; + boolean cob=ArchitectureConfig.cob,frames=ArchitectureConfig.frames,thatch=ArchitectureConfig.thatch,slate=ArchitectureConfig.slate,windows=ArchitectureConfig.windows; + zone.moddev.mc.ironagearchitecture.EraSelection eras=ArchitectureConfig.survivalEras; java.nio.file.Path directory=Paths.get("config-matrix");Files.createDirectories(directory); try { - for(int mask=0;mask<16;mask++) { + for(int mask=0;mask<32;mask++) { java.nio.file.Path file=directory.resolve(mask+".cfg"); String text="blocks {\n B:cob="+((mask&1)!=0)+"\n B:timberFrames="+((mask&2)!=0) - +"\n B:thatch="+((mask&4)!=0)+"\n B:slate="+((mask&8)!=0)+"\n}\n"; + +"\n B:thatch="+((mask&4)!=0)+"\n B:slate="+((mask&8)!=0)+"\n B:leadedWindows="+((mask&16)!=0)+"\n}\n"; Files.write(file,text.getBytes(StandardCharsets.UTF_8));ArchitectureConfig.load(file.toFile()); require(ArchitectureConfig.cob==((mask&1)!=0)&&ArchitectureConfig.frames==((mask&3)==3) - &&ArchitectureConfig.thatch==((mask&4)!=0)&&ArchitectureConfig.slate==((mask&8)!=0),"Config mask "+mask); + &&ArchitectureConfig.thatch==((mask&4)!=0)&&ArchitectureConfig.slate==((mask&8)!=0)&&ArchitectureConfig.windows==((mask&19)==19),"Config mask "+mask); } - } finally {ArchitectureConfig.cob=cob;ArchitectureConfig.frames=frames;ArchitectureConfig.thatch=thatch;ArchitectureConfig.slate=slate;} + } finally {ArchitectureConfig.cob=cob;ArchitectureConfig.frames=frames;ArchitectureConfig.thatch=thatch;ArchitectureConfig.slate=slate;ArchitectureConfig.windows=windows;ArchitectureConfig.survivalEras=eras;} + } + private void verifyWallHeads(WorldServer world) { + BlockPos pos=new BlockPos(95,90,95); + for(String wood:ArchitectureBlocks.WOODS) for(EnumFacing facing:EnumFacing.HORIZONTALS) { + JettyFloorBlock floor=ArchitectureBlocks.findFloor(wood,FloorShape.FULL,null); + IBlockState deck=floor.getDefaultState().withProperty(FrameBlock.FACING,facing); + world.setBlockState(pos,deck,2); + for(PartialWallBlock wall:ArchitectureBlocks.PARTIALS) if(wood.equals(wall.wood)) { + IBlockState below=wall.getDefaultState().withProperty(FrameBlock.FACING,facing); + world.setBlockState(pos.down(),below,2); + int joins=floor.connections(deck,world,pos); + for(int q=0;q<4;q++) { + double x=q%2*8+4,z=q/2*8+4; + require(((joins>>(16+q))&1)==(wall.profile.contains(x,z)?1:0),"Wall-head footprint "+wall+" "+facing); + double[] point=WallGeometry.rotate(x,4,z,facing);boolean filled=false; + for(WallGeometry.Part part:floor.parts(deck,joins)) if(part.bounds(facing).isVecInside(new net.minecraft.util.math.Vec3d(point[0]/16,point[1]/16,point[2]/16))) filled=true; + require(filled==wall.profile.contains(x,z),"Wall-head opening/cantilever air "+wall+" "+q); + } + } + world.setBlockToAir(pos.down()); + require((floor.connections(deck,world,pos)>>16)==0,"Stale wall-head beam"); + world.setBlockState(pos.down(),Blocks.STONE.getDefaultState(),2); + require((floor.connections(deck,world,pos)>>16)==15,"Solid lower-wall head"); + world.setBlockToAir(pos);world.setBlockToAir(pos.down()); + } } private void verifyPlacementAndWalking(WorldServer world) { FakePlayer player=FakePlayerFactory.getMinecraft(world); diff --git a/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/ClientProbe.java b/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/ClientProbe.java index 96537f9..14e410e 100644 --- a/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/ClientProbe.java +++ b/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/ClientProbe.java @@ -2,6 +2,11 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.inventory.GuiContainerCreative; +import net.minecraft.creativetab.CreativeTabs; +import zone.moddev.mc.ironagearchitecture.Era; +import zone.moddev.mc.ironagearchitecture.EraTabs; import net.minecraft.client.renderer.block.model.IBakedModel; import net.minecraft.block.state.IBlockState; import net.minecraft.util.ScreenShotHelper; @@ -16,13 +21,17 @@ import zone.moddev.mc.ironagearchitecture.block.RoofBlock; import zone.moddev.mc.ironagearchitecture.client.RoofBakedModel; import zone.moddev.mc.ironagearchitecture.block.FrameBlock; +import zone.moddev.mc.ironagearchitecture.block.JettyFloorBlock; +import zone.moddev.mc.ironagearchitecture.block.WallGeometry; import zone.moddev.mc.ironagearchitecture.client.WallBakedModel; +import zone.moddev.mc.ironagearchitecture.block.WindowBlock; +import zone.moddev.mc.ironagearchitecture.client.WindowBakedModel; import net.minecraft.item.ItemStack; import net.minecraft.client.renderer.block.model.ModelResourceLocation; /** Opt-in native client capture. No registration or launch path in the player mod. */ public final class ClientProbe extends ProbeProxy { - private int phase, ticks; + private int phase, ticks, paletteRetries; @Override public void init() { if(Boolean.getBoolean("iaa.clientProbe")) FMLCommonHandler.instance().bus().register(this); } @SubscribeEvent public void tick(TickEvent.ClientTickEvent event) { if(event.phase!=TickEvent.Phase.END) return; @@ -31,7 +40,8 @@ public final class ClientProbe extends ProbeProxy { if(phase==0&&mc.currentScreen instanceof GuiMainMenu&&ticks>60) { mc.gameSettings.pauseOnLostFocus=false; phase=1;ticks=0; - mc.launchIntegratedServer("Jettied Street Preview","Jettied Street Preview",new WorldSettings(137,GameType.CREATIVE,false,false,WorldType.FLAT)); + String preview=System.getProperty("iaa.previewName","Horizontal Rails Preview"); + mc.launchIntegratedServer(preview,preview,new WorldSettings(137,GameType.CREATIVE,false,false,WorldType.FLAT)); } else if(phase==1&&mc.player!=null) { phase=2;ticks=0; mc.displayGuiScreen(null);mc.gameSettings.hideGUI=true;mc.gameSettings.renderDistanceChunks=8; @@ -48,27 +58,114 @@ public final class ClientProbe extends ProbeProxy { if(!(model instanceof WallBakedModel)||model.getQuads(state,null,0).isEmpty()) throw new IllegalStateException("Wall model missing "+state); for(net.minecraft.client.renderer.block.model.BakedQuad quad:model.getQuads(state,null,0)) if(quad.getSprite().getIconName().contains("missingno")) throw new IllegalStateException("Missing material sprite "+state); + if(wall instanceof JettyFloorBlock) verifyBracketShell((JettyFloorBlock)wall,state,model); } for(int meta=0;ArchitectureBlocks.VISIBLE.contains(wall)&&meta120) { capture(mc,"half-block-corner.png");lookAt(mc,24,65.6,-3.5,24,68.7,-.2);phase=4;ticks=0; } else if(phase==4&&ticks>120) { capture(mc,"whole-block-underside.png"); - lookAt(mc,34,81,-10,43,73,1);phase=5;ticks=0; + lookAt(mc,23.5,66.1,-1.8,24.5,68.2,-.35);phase=40;ticks=0; + } else if(phase==40&&ticks>100) { + capture(mc,"full-bracket-left.png");lookAt(mc,25.7,66.1,-1.8,24.5,68.2,-.35);phase=41;ticks=0; + } else if(phase==41&&ticks>100) { + capture(mc,"full-bracket-right.png");lookAt(mc,24.5,65.9,-2,24.5,68.25,-.3);phase=42;ticks=0; + } else if(phase==42&&ticks>100) { + capture(mc,"full-bracket-base.png");lookAt(mc,4.5,66,-1.6,4.5,68.2,.1);phase=43;ticks=0; + } else if(phase==43&&ticks>100) { + capture(mc,"half-bracket-base.png");lookAt(mc,34,81,-10,43,73,1);phase=5;ticks=0; } else if(phase==5&&ticks>120) { capture(mc,"successive-half-jetties.png");lookAt(mc,55,80,-9,64,72,1);phase=6;ticks=0; } else if(phase==6&&ticks>120) { capture(mc,"two-sided-jetty.png");lookAt(mc,5,71,5,5,70,-1);phase=7;ticks=0; } else if(phase==7&&ticks>120) { - capture(mc,"upper-room-interior.png"); - System.out.println("IAA_CLIENT_PROBE_PASS");phase=8; - mc.shutdown(); + capture(mc,"upper-room-interior.png");lookAt(mc,11,72,-13,12,69,0);phase=50;ticks=0; + } else if(phase>=50&&phase<=57&&ticks>120) { + String[] names={"glazed-house-and-comparison.png","closed-diamond-window.png","open-rectangular-casement.png","window-from-interior.png","leaded-window-woods.png","chunk-border-open-casement.png","horizontal-window-corner.png","horizontal-window-interior.png"}; + double[][] cameras={{3,66,-3,3,66.7,.15},{4.5,71,-3,3.5,70.8,-.3},{3,70,3,3,70.7,-.25},{15,68,18,15,65,23},{19,66,26,15.5,65,27.5},{-2,70,-3,1.3,70.9,-.35},{1.8,70,3,1,70.9,-.3}}; + capture(mc,names[phase-50]); + if(phase<57){double[] c=cameras[phase-50];lookAt(mc,c[0],c[1],c[2],c[3],c[4],c[5]);phase++;ticks=0;return;} + for(Era era:Era.values()) { + java.util.List items=new java.util.ArrayList<>();EraTabs.tab(era).displayAllRelevantItems(items); + int expected=era==Era.IRON_AGE?14:510; + if(items.size()!=expected) throw new IllegalStateException("Creative tab "+era+" expected "+expected+" got "+items.size()); + for(ItemStack item:items) if(!zone.moddev.mc.ironagearchitecture.EraCatalog.eras(item.getItem()).contains(era)) throw new IllegalStateException("Wrong era item "+item); + for(ItemStack item:items) { + java.util.List tooltip=new java.util.ArrayList<>();item.getItem().addInformation(item,mc.player,tooltip,false); + if(tooltip.isEmpty()) throw new IllegalStateException("Missing crafting hint "+item); + for(String line:tooltip) if(line.startsWith("hint.ironagearchitecture.")) throw new IllegalStateException("Untranslated crafting hint "+line); + } + } + mc.displayGuiScreen(new PaletteGui(mc,Era.IRON_AGE,true));phase=8;ticks=0; + } else if(phase==8) { + if(!ensurePalette(mc,Era.IRON_AGE)) {ticks=0;return;} + if(ticks>80) {capture(mc,"iron-age-palette.png");mc.displayGuiScreen(new PaletteGui(mc,Era.LATE_MEDIEVAL_TUDOR,false));phase=9;ticks=0;} + } else if(phase==9) { + if(!ensurePalette(mc,Era.LATE_MEDIEVAL_TUDOR)) {ticks=0;return;} + if(ticks>80) { + capture(mc,"late-medieval-tudor-palette.png");mc.displayGuiScreen(null); + System.out.println("IAA_CLIENT_PROBE_PASS creativeIronAge=14 creativeLateMedievalTudor=510");phase=10; + mc.shutdown(); + } + } + } + private void verifyBracketShell(JettyFloorBlock floor,IBlockState state,IBakedModel model) { + if(floor.bracket==null) return; + java.util.List parts=floor.parts(state,0); + WallGeometry.Part bracket=parts.get(parts.size()-1); + if(bracket.horizontal||bracket.grain!=WallGeometry.Grain.FALLING) return; + double x=0,y=0;for(double[] p:bracket.outline) {x+=p[0]/bracket.outline.length;y+=p[1]/bracket.outline.length;} + double[] centre=bracket.transform(x,y,(bracket.front+bracket.back)/2,state.getValue(FrameBlock.FACING)); + int checked=0; + for(net.minecraft.client.renderer.block.model.BakedQuad quad:model.getQuads(state,null,0)) { + int[] data=quad.getVertexData();int stride=data.length/4;double[][] points=new double[4][3];double height=0; + for(int v=0;v<4;v++) for(int axis=0;axis<3;axis++) {points[v][axis]=16*Float.intBitsToFloat(data[v*stride+axis]);if(axis==1) height+=points[v][axis]/4;} + if(height>=7.99) continue; // Floor timbers and corner ties are above the bracket. + double[] a=points[0];double dot=0; + for(int i=1;i<3&&Math.abs(dot)<1e-7;i++) { + double[] b=points[i],c=points[i+1]; + double nx=(b[1]-a[1])*(c[2]-a[2])-(b[2]-a[2])*(c[1]-a[1]); + double ny=(b[2]-a[2])*(c[0]-a[0])-(b[0]-a[0])*(c[2]-a[2]); + double nz=(b[0]-a[0])*(c[1]-a[1])-(b[1]-a[1])*(c[0]-a[0]); + dot=nx*(a[0]-centre[0])+ny*(a[1]-centre[1])+nz*(a[2]-centre[2]); + } + if(dot<=1e-7) throw new IllegalStateException("Inward or degenerate baked bracket face "+state+" dot="+dot+" centre="+java.util.Arrays.toString(centre)+" points="+java.util.Arrays.deepToString(points)); + checked++; + } + if(checked<5) throw new IllegalStateException("Incomplete baked bracket shell "+state+" faces="+checked); + } + private boolean ensurePalette(Minecraft mc,Era era) { + if(mc.currentScreen instanceof PaletteGui&&((PaletteGui)mc.currentScreen).getSelectedTabIndex()==EraTabs.tab(era).getTabIndex()) {paletteRetries=0;return true;} + if(++paletteRetries==1) System.out.println("IAA_PALETTE_RETRY screen="+(mc.currentScreen==null?"null":mc.currentScreen.getClass().getName())+" creative="+mc.playerController.isInCreativeMode()+" selected="+(mc.currentScreen instanceof GuiContainerCreative?((GuiContainerCreative)mc.currentScreen).getSelectedTabIndex():-1)+" desired="+EraTabs.tab(era).getTabIndex()); + if(paletteRetries>100) throw new IllegalStateException("Creative palette would not remain open "+era); + mc.displayGuiScreen(new PaletteGui(mc,era,false));return false; + } + private static final class PaletteGui extends GuiContainerCreative { + private final Era era;private final boolean nextPage; + PaletteGui(Minecraft mc,Era era,boolean nextPage) {super(mc.player);this.era=era;this.nextPage=nextPage;} + @Override public void initGui() { + super.initGui(); + try { + if(nextPage) for(GuiButton button:buttonList) if(">".equals(button.displayString)&&button.enabled) {super.actionPerformed(button);break;} + boolean selected=false; + for(java.lang.reflect.Method method:GuiContainerCreative.class.getDeclaredMethods()) + if(java.lang.reflect.Modifier.isPrivate(method.getModifiers())&&method.getReturnType()==Void.TYPE&&java.util.Arrays.equals(method.getParameterTypes(),new Class[]{CreativeTabs.class})) { + method.setAccessible(true);method.invoke(this,EraTabs.tab(era));selected=true;break; + } + if(!selected) throw new IllegalStateException("Creative tab setter missing"); + if(getSelectedTabIndex()!=EraTabs.tab(era).getTabIndex()) throw new IllegalStateException("Creative tab selection did not stick"); + } catch(Exception failure) {throw new IllegalStateException("Palette preview failed",failure);} } } private void lookAt(Minecraft mc,double x,double y,double z,double tx,double ty,double tz) { diff --git a/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/EraProbe.java b/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/EraProbe.java new file mode 100644 index 0000000..412678f --- /dev/null +++ b/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/EraProbe.java @@ -0,0 +1,77 @@ +package zone.moddev.mc.ironagearchitecture.probe; + +import java.util.*; +import java.nio.file.*; +import java.nio.charset.StandardCharsets; +import net.minecraft.inventory.*; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.*; +import net.minecraft.item.crafting.*; +import net.minecraft.world.World; +import zone.moddev.mc.ironagearchitecture.*; +import zone.moddev.mc.ironagearchitecture.block.FrameBlock; + +/** Runs against Forge's actual crafting manager and parsed config files. */ +final class EraProbe { + private static void require(boolean condition,String message) {if(!condition) throw new IllegalStateException(message);} + static int expectedRecipes() { + return ArchitectureConfig.survivalEras.allowed.contains(Era.LATE_MEDIEVAL_TUDOR)?(ArchitectureConfig.windows?2010:1863): + ArchitectureConfig.survivalEras.allowed.contains(Era.IRON_AGE)?36:0; + } + private static boolean ours(IRecipe recipe) { + ItemStack output=recipe.getRecipeOutput(); + return output!=null&&"ironagearchitecture".equals(output.getItem().getRegistryName().getResourceDomain()); + } + private static InventoryCrafting grid(IRecipe recipe) { + InventoryCrafting grid=new InventoryCrafting(new Container(){@Override public boolean canInteractWith(EntityPlayer player){return true;}},3,3); + if(recipe instanceof ShapedRecipes) { + ShapedRecipes r=(ShapedRecipes)recipe; + for(int y=0;y inputs=((ShapelessRecipes)recipe).recipeItems; + for(int i=0;i recipes=CraftingManager.getInstance().getRecipeList(); + List original=new ArrayList<>(recipes); + EraSelection selected=ArchitectureConfig.survivalEras; + boolean cob=ArchitectureConfig.cob,frames=ArchitectureConfig.frames,thatch=ArchitectureConfig.thatch,slate=ArchitectureConfig.slate,windows=ArchitectureConfig.windows; + String[][] cases={{"all"},{"iron_age"},{"late_medieval_tudor"},{"iron_age","late_medieval_tudor"},{},{"typo"},{"iron_age","typo"},{" IRON_AGE ","iron_age"}}; + try { + recipes.removeIf(EraProbe::ours);ArchitectureConfig.survivalEras=EraSelection.parse("all");ArchitectureRecipes.register(); + List baseline=new ArrayList<>();for(IRecipe recipe:recipes) if(ours(recipe)) baseline.add(recipe); + require(baseline.size()==expectedRecipes(),"Era baseline "+baseline.size()); + Files.createDirectories(Paths.get("era-matrix")); + for(int c=0;c\n}\n"; + Files.write(file,text.getBytes(StandardCharsets.UTF_8));ArchitectureConfig.load(file.toFile()); + recipes.removeIf(EraProbe::ours);ArchitectureRecipes.register(); + int count=0;for(IRecipe recipe:recipes) if(ours(recipe)) {require(EraCatalog.canCraft(recipe.getRecipeOutput()),"Prohibited registered output");count++;} + require(count==expectedRecipes(),"Era recipe count case "+c+": "+count); + for(IRecipe recipe:baseline) { + ItemStack expected=recipe.getRecipeOutput(),actual=CraftingManager.getInstance().findMatchingRecipe(grid(recipe),world); + if(EraCatalog.canCraft(expected)) require(actual!=null&&ItemStack.areItemStacksEqual(actual,expected),"Allowed recipe unavailable case "+c+" "+expected); + else require(actual==null||!"ironagearchitecture".equals(actual.getItem().getRegistryName().getResourceDomain())||EraCatalog.canCraft(actual),"Restricted output crafted case "+c); + } + int iron=0,late=0; + for(ItemStack stack:ArchitectureBlocks.visibleStacks()) { + if(EraCatalog.eras(stack.getItem()).contains(Era.IRON_AGE)) iron++; + if(EraCatalog.eras(stack.getItem()).contains(Era.LATE_MEDIEVAL_TUDOR)) late++; + } + require(iron==14&&late==(windows?510:460),"Creative palette changed with recipe filter"); + int expected=ArchitectureConfig.survivalEras.allowed.contains(Era.LATE_MEDIEVAL_TUDOR)?(windows?510:460): + ArchitectureConfig.survivalEras.allowed.contains(Era.IRON_AGE)?14:0; + require(RecipeProbe.verifyReachability()==expected,"Era selection leaves unreachable content case "+c); + } + System.out.println("IAA_ERA_MATRIX_PASS cases="+cases.length+" sharedVariants=14 lateVariants="+(windows?510:460)); + } finally { + recipes.clear();recipes.addAll(original);ArchitectureConfig.survivalEras=selected; + ArchitectureConfig.cob=cob;ArchitectureConfig.frames=frames;ArchitectureConfig.thatch=thatch;ArchitectureConfig.slate=slate;ArchitectureConfig.windows=windows; + } + } +} diff --git a/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/HorizontalRailProbe.java b/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/HorizontalRailProbe.java new file mode 100644 index 0000000..703f459 --- /dev/null +++ b/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/HorizontalRailProbe.java @@ -0,0 +1,57 @@ +package zone.moddev.mc.ironagearchitecture.probe; + +import java.util.*; +import net.minecraft.block.state.IBlockState; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.*; +import net.minecraft.world.World; +import zone.moddev.mc.ironagearchitecture.*; +import zone.moddev.mc.ironagearchitecture.block.*; + +/** Measure timber contact across the shared block face, rather than trusting connection bits. */ +final class HorizontalRailProbe { + static void verify(World world) { + BlockPos base=new BlockPos(15,105,-80);int links=0; + for(String wood:ArchitectureBlocks.WOODS) for(int finish=0;finish<2;finish++) for(EnumFacing facing:EnumFacing.HORIZONTALS) + for(WallProfile profile:new WallProfile[]{null,WallProfile.CORNER_L,WallProfile.CORNER_HALF_LEFT,WallProfile.CORNER_HALF_RIGHT,WallProfile.CORNER_QUARTER}) + for(boolean window:new boolean[]{false,true}) for(int height=0;height<2;height++) { + if(window&&!ArchitectureConfig.windows) continue; + EnumFacing right=facing.rotateY();BlockPos middle=base.offset(right),end=base.offset(right,2); + clear(world,base);clear(world,middle);clear(world,end); + FrameBlock corner=profile==null?ArchitectureBlocks.findWall(wood,FrameFamily.TERMINAL):ArchitectureBlocks.findPartial(wood,FrameFamily.TERMINAL,profile); + EnumFacing plane=profile!=null&&profile.z0==8?facing.getOpposite():facing; + FrameBlock post=profile==null?ArchitectureBlocks.findWall(wood,FrameFamily.POST):ArchitectureBlocks.findPartial(wood,FrameFamily.POST,WallProfile.THIN); + IBlockState cs=corner.getDefaultState().withProperty(FrameBlock.FACING,facing).withProperty(FrameBlock.PANEL,finish*2),ps=post.getDefaultState().withProperty(FrameBlock.FACING,plane).withProperty(FrameBlock.PANEL,finish*2); + IBlockState source; + if(window) source=ArchitectureBlocks.findWindow(wood,finish,profile!=null,true,false,profile!=null&&profile.z0==8).getDefaultState().withProperty(WindowBlock.FACING,facing); + else {FrameBlock rail=profile==null?ArchitectureBlocks.findWall(wood,FrameFamily.RAIL):ArchitectureBlocks.findPartial(wood,FrameFamily.RAIL,WallProfile.THIN);source=rail.getDefaultState().withProperty(FrameBlock.FACING,plane).withProperty(FrameBlock.PANEL,finish*2+height);} + world.setBlockState(base,cs,2);world.setBlockState(middle,ps,2);world.setBlockState(end,source,2); + if(!touch(world,base,middle,right,height)||!touch(world,middle,end,right,height)) throw new IllegalStateException("Horizontal rail gap: "+wood+" "+finish+" "+facing+" "+profile+" window="+window+" height="+height+" cornerMask="+corner.connections(cs,world,base)+" postMask="+post.connections(ps,world,middle)); + world.setBlockToAir(end); + if((corner.connections(cs,world,base)&0xff0)!=0||(post.connections(ps,world,middle)&0xff0)!=0) throw new IllegalStateException("Rail remained after source removal"); + links++; + } + clear(world,base);for(EnumFacing side:EnumFacing.HORIZONTALS) for(int d=1;d<=3;d++) clear(world,base.offset(side,d)); + System.out.println("IAA_HORIZONTAL_RAIL_PASS linkedRuns="+links+" measuredFaces="+(links*2)); + } + private static List timber(World world,BlockPos at,int height) { + IBlockState state=world.getBlockState(at);List result=new ArrayList<>(); + double y0=at.getY()+(height==0?0:13.0/16),y1=at.getY()+(height==0?3.0/16:1); + if(state.getBlock() instanceof FrameBlock) { + FrameBlock block=(FrameBlock)state.getBlock();for(WallGeometry.Part part:block.parts(state,block.connections(state,world,at),TimberCorners.derive(state,world,at))) if(part.grain!=WallGeometry.Grain.DAUB) result.add(part.bounds(state.getValue(FrameBlock.FACING)).offset(at)); + } else if(state.getBlock() instanceof WindowBlock) { + WindowBlock block=(WindowBlock)state.getBlock();result.addAll(block.boxes(state,world,at,false));for(int i=0;iMath.min(b.maxY,y1)-Math.max(b.minY,y0)<1e-6);return result; + } + private static boolean touch(World world,BlockPos a,BlockPos b,EnumFacing direction,int height) { + for(AxisAlignedBB x:timber(world,a,height)) for(AxisAlignedBB y:timber(world,b,height)) { + boolean alongX=direction.getAxis()==EnumFacing.Axis.X,positive=direction.getAxisDirection()==EnumFacing.AxisDirection.POSITIVE; + double edge=alongX?(positive?x.maxX:x.minX):(positive?x.maxZ:x.minZ),other=alongX?(positive?y.minX:y.maxX):(positive?y.minZ:y.maxZ); + double overlap=alongX?Math.min(x.maxZ,y.maxZ)-Math.max(x.minZ,y.minZ):Math.min(x.maxX,y.maxX)-Math.max(x.minX,y.minX); + if(Math.abs(edge-other)<1e-6&&overlap>1e-6&&Math.min(x.maxY,y.maxY)-Math.max(x.minY,y.minY)>1e-6) return true; + } + return false; + } + private static void clear(World world,BlockPos pos){world.setBlockToAir(pos);world.setBlockToAir(pos.up());world.setBlockToAir(pos.down());} +} diff --git a/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/JettyPreview.java b/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/JettyPreview.java index 95825bd..db59094 100644 --- a/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/JettyPreview.java +++ b/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/JettyPreview.java @@ -112,7 +112,9 @@ private void placeWall(World world,BlockPos pos,String wood,int mask,boolean cor for(WallProfile profile:new WallProfile[]{WallProfile.THIN,WallProfile.SHORT_LEFT,WallProfile.SHORT_RIGHT}) for(EnumFacing facing:EnumFacing.HORIZONTALS) { if(footprint(profile,facing)!=mask) continue; PartialWallBlock block=ArchitectureBlocks.findPartial(family==FrameFamily.INFILL?null:wood,family,profile); - int panel=family==FrameFamily.INFILL||family==FrameFamily.TERMINAL?finish:finish*2+pattern; + // The house plan describes a world-space diagonal; a rear-half strip reverses local X. + int placedPattern=family==FrameFamily.BRACE&&facing.rotateY().getAxisDirection()==EnumFacing.AxisDirection.NEGATIVE?pattern^1:pattern; + int panel=family==FrameFamily.INFILL||family==FrameFamily.TERMINAL?finish:finish*2+placedPattern; world.setBlockState(pos,block.getDefaultState().withProperty(FrameBlock.FACING,facing).withProperty(FrameBlock.PANEL,panel),2);return; } throw new IllegalStateException("Unrepresented wall footprint "+mask+" at "+pos); diff --git a/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/RecipeProbe.java b/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/RecipeProbe.java new file mode 100644 index 0000000..696c6c9 --- /dev/null +++ b/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/RecipeProbe.java @@ -0,0 +1,129 @@ +package zone.moddev.mc.ironagearchitecture.probe; + +import java.util.*; +import net.minecraft.block.Block; +import net.minecraft.init.Blocks; +import net.minecraft.init.Items; +import net.minecraft.inventory.*; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.*; +import net.minecraft.item.crafting.*; +import net.minecraft.world.World; +import zone.moddev.mc.ironagearchitecture.*; +import zone.moddev.mc.ironagearchitecture.block.*; + +/** Player-facing recipe contracts checked in initialized, packaged Forge. */ +final class RecipeProbe { + private static void require(boolean ok,String message) {if(!ok) throw new IllegalStateException(message);} + private static boolean ours(ItemStack stack) {return stack!=null&&"ironagearchitecture".equals(stack.getItem().getRegistryName().getResourceDomain());} + private static String key(ItemStack stack) {return stack.getItem().getRegistryName()+"@"+stack.getMetadata();} + private static InventoryCrafting grid() {return new InventoryCrafting(new Container(){@Override public boolean canInteractWith(EntityPlayer player){return true;}},3,3);} + private static List inputs(IRecipe recipe) { + List result=new ArrayList<>(); + if(recipe instanceof ShapedRecipes) {for(ItemStack input:((ShapedRecipes)recipe).recipeItems) if(input!=null) result.add(input);} + else if(recipe instanceof ShapelessRecipes) result.addAll(((ShapelessRecipes)recipe).recipeItems); + else throw new IllegalStateException("Unexpected recipe "+recipe); + return result; + } + private static void crafts(World world,ItemStack expected,ItemStack... slots) { + InventoryCrafting grid=grid();for(int i=0;i ingredients=inputs(recipe); + boolean conversion=true;Map before=new TreeMap<>(); + for(ItemStack input:ingredients) { + if(!ours(input)) {conversion=false;break;} + resources(before,input,1); + } + if(conversion) { + ItemStack output=recipe.getRecipeOutput();Map after=new TreeMap<>();resources(after,output,output.stackSize); + require(before.equals(after),"Material changes in conversion "+output+": "+before+" -> "+after);pure++; + } + if(recipe instanceof ShapedRecipes) { + ShapedRecipes shaped=(ShapedRecipes)recipe; + for(int y=0;y<=3-shaped.recipeHeight;y++) for(int x=0;x<=3-shaped.recipeWidth;x++) for(boolean mirror:new boolean[]{false,true}) { + InventoryCrafting grid=grid(); + for(int row=0;row reachable,ItemStack input) { + if(input.getMetadata()!=net.minecraftforge.oredict.OreDictionary.WILDCARD_VALUE) return reachable.contains(key(input)); + String prefix=input.getItem().getRegistryName()+"@"; + for(String candidate:reachable) if(candidate.startsWith(prefix)) return true; + return false; + } + private static void add(Map quantities,String material,int units) {quantities.put(material,quantities.getOrDefault(material,0)+units);} + private static void resources(Map quantities,ItemStack stack,int count) { + if(stack.getItem() instanceof LeadedPaneItem) {add(quantities,"leaded-glazing",count);return;} + Block block=Block.getBlockFromItem(stack.getItem()); + if(block instanceof WindowBlock) {WindowBlock window=(WindowBlock)block;add(quantities,"frame:"+window.wood+":"+window.finish,(window.thin?2:4)*count);add(quantities,"leaded-glazing",count);return;} + add(quantities,material(stack),units(stack)*count); + } + private static String material(ItemStack stack) { + Item item=stack.getItem(); + if(item instanceof JettyBracketItem) return "floor:"+((JettyBracketItem)item).wood; + Block block=Block.getBlockFromItem(item); + if(block instanceof JettyFloorBlock) return "floor:"+((JettyFloorBlock)block).wood; + if(block instanceof FrameBlock) {FrameBlock wall=(FrameBlock)block;return wall.family==FrameFamily.INFILL?"daub:"+wall.finish(stack.getMetadata()):"frame:"+wall.wood+":"+wall.finish(stack.getMetadata());} + for(int finish=0;finish<2;finish++) if(ArchitectureBlocks.COB.get(finish)==block||ArchitectureBlocks.SLABS.get(finish)==block||ArchitectureBlocks.STAIRS.get(finish)==block) return "daub:"+finish; + if(block instanceof RoofBlock) return "roof:"+(block.getRegistryName().getResourcePath().startsWith("thatch")?"thatch":"slate"); + throw new IllegalStateException("Unknown recipe material "+stack); + } + private static int units(ItemStack stack) { + Item item=stack.getItem();if(item instanceof JettyBracketItem) return ((JettyBracketItem)item).fullReach?2:1; + Block block=Block.getBlockFromItem(item); + if(block instanceof JettyFloorBlock) return Integer.bitCount(((JettyFloorBlock)block).shape.mask); + if(block instanceof PartialWallBlock) {WallProfile p=((PartialWallBlock)block).profile;return p.corner?p.quarters():(p==WallProfile.THIN?2:1);} + if(ArchitectureBlocks.SLABS.contains(block)) return 2; + if(ArchitectureBlocks.STAIRS.contains(block)) return 3; + if(block instanceof RoofBlock) return ((RoofBlock)block).isRidge()?2:4; + return 4; + } +} diff --git a/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/TimberJoinProbe.java b/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/TimberJoinProbe.java new file mode 100644 index 0000000..5bf6bf3 --- /dev/null +++ b/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/TimberJoinProbe.java @@ -0,0 +1,66 @@ +package zone.moddev.mc.ironagearchitecture.probe; + +import net.minecraft.block.state.IBlockState; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraft.util.EnumFacing; +import zone.moddev.mc.ironagearchitecture.ArchitectureBlocks; +import zone.moddev.mc.ironagearchitecture.block.*; + +/** Check actual generated bay endpoints, including the reversed local axis of rear-half walls. */ +final class TimberJoinProbe { + static void preview(World world) { + int bays=0; + for(int house:new int[]{0,40,62}) for(int floor=0;floor<(house==0?2:3);floor++) { + double offset=floor==0?0:floor==1?.5:house==62?1.5:1; + BlockPos low=new BlockPos(house+5,65+floor*5,(int)Math.floor(-offset)),high=low.east().up(); + if(!endpoint(world,low,house+6,66+floor*5)||!endpoint(world,high,house+6,66+floor*5)) + throw new IllegalStateException("Preview brace bay does not meet: "+low+" "+world.getBlockState(low)+" / "+high+" "+world.getBlockState(high)); + for(BlockPos at:new BlockPos[]{low.up(),low.east()}) if(TimberCorners.derive(world.getBlockState(at),world,at)==0) + throw new IllegalStateException("Point-only brace join lacks a corner continuation at "+at); + bays++; + } + verifyCorners(world); + System.out.println("IAA_TIMBER_JOIN_PREVIEW_PASS continuousBraceBays="+bays+" cornerCases=96"); + } + private static void verifyCorners(World world) { + BlockPos at=new BlockPos(15,110,-64); + for(int wood=0;wood<6;wood++) for(boolean thin:new boolean[]{false,true}) for(EnumFacing facing:EnumFacing.HORIZONTALS) for(int slope=0;slope<2;slope++) { + FrameBlock host=thin?ArchitectureBlocks.findPartial(null,FrameFamily.INFILL,WallProfile.THIN):ArchitectureBlocks.DAUB; + FrameBlock brace=thin?ArchitectureBlocks.findPartial(ArchitectureBlocks.WOODS[wood],FrameFamily.BRACE,WallProfile.THIN):ArchitectureBlocks.findWall(ArchitectureBlocks.WOODS[wood],FrameFamily.BRACE); + IBlockState plain=host.getDefaultState().withProperty(FrameBlock.FACING,facing),beam=brace.getDefaultState().withProperty(FrameBlock.FACING,facing).withProperty(FrameBlock.PANEL,slope); + BlockPos h=at.offset(facing.rotateY()),v=slope==0?at.down():at.up(); + for(EnumFacing side:EnumFacing.values()) world.setBlockToAir(at.offset(side)); + world.setBlockState(at,plain,2);world.setBlockState(h,beam,2);world.setBlockState(v,beam,2); + int expected=(wood+1)<<((slope==0?1:3)*3),actual=TimberCorners.derive(plain,world,at); + if(actual!=expected) throw new IllegalStateException("Missing/incorrect diagonal corner "+thin+" "+facing+" "+slope+" expected="+expected+" actual="+actual); + java.util.List parts=host.parts(plain,host.connections(plain,world,at),actual);int patches=0; + for(WallGeometry.Part part:parts) if(part.timber!=null) {patches++;if(!part.timber.equals(ArchitectureBlocks.WOODS[wood])) throw new IllegalStateException("Wrong infill corner timber");} + if(patches!=2) throw new IllegalStateException("Both elevations need a corner continuation"); + world.setBlockToAir(h);if(TimberCorners.derive(plain,world,at)!=0) throw new IllegalStateException("One brace alone produced a corner"); + world.setBlockState(h,beam,2);world.setBlockState(v,beam.withProperty(FrameBlock.PANEL,1-slope),2); + if(TimberCorners.derive(plain,world,at)!=0) throw new IllegalStateException("Incompatible slopes produced a corner"); + FrameBlock other=thin?ArchitectureBlocks.findPartial(ArchitectureBlocks.WOODS[(wood+1)%6],FrameFamily.BRACE,WallProfile.THIN):ArchitectureBlocks.findWall(ArchitectureBlocks.WOODS[(wood+1)%6],FrameFamily.BRACE); + world.setBlockState(v,other.getDefaultState().withProperty(FrameBlock.FACING,facing).withProperty(FrameBlock.PANEL,slope),2); + if(TimberCorners.derive(plain,world,at)!=0) throw new IllegalStateException("Mixed woods produced a corner"); + world.setBlockState(v,beam.withProperty(FrameBlock.FACING,facing.getOpposite()),2); + if(TimberCorners.derive(plain,world,at)!=0) throw new IllegalStateException("Offset/opposite brace produced a corner"); + world.setBlockToAir(at);for(EnumFacing side:EnumFacing.values()) world.setBlockToAir(at.offset(side)); + } + } + private static boolean endpoint(World world,BlockPos pos,double x,double y) { + IBlockState state=world.getBlockState(pos);if(!(state.getBlock() instanceof FrameBlock)) return false; + FrameBlock block=(FrameBlock)state.getBlock();if(block.family!=FrameFamily.BRACE) return false; + int faces=0; + for(WallGeometry.Part part:block.parts(state,block.connections(state,world,pos))) { + if(part.grain!=WallGeometry.Grain.RISING&&part.grain!=WallGeometry.Grain.FALLING) continue; + boolean found=false; + for(double[] p:part.outline) { + double[] point=part.transform(p[0],p[1],part.front,state.getValue(FrameBlock.FACING)); + if(Math.abs(pos.getX()+point[0]/16-x)<1e-7&&Math.abs(pos.getY()+point[1]/16-y)<1e-7) found=true; + } + if(found) faces++; + } + return faces>=2; + } +} diff --git a/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/WindowPreview.java b/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/WindowPreview.java new file mode 100644 index 0000000..2000c36 --- /dev/null +++ b/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/WindowPreview.java @@ -0,0 +1,41 @@ +package zone.moddev.mc.ironagearchitecture.probe; + +import java.util.*; +import net.minecraft.block.state.IBlockState; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import zone.moddev.mc.ironagearchitecture.ArchitectureBlocks; +import zone.moddev.mc.ironagearchitecture.block.*; + +/** The first and third houses are glazed; the second is retained for comparison. */ +final class WindowPreview { + static void build(World world) { + for(int x=-2;x<=42;x++) for(int z=21;z<=32;z++) world.setBlockState(new BlockPos(x,63,z),(z==25?net.minecraft.init.Blocks.COBBLESTONE:net.minecraft.init.Blocks.GRASS).getDefaultState(),2); + house(world,0,"dark_oak",new double[]{0,.5},1); + house(world,40,"dark_oak",new double[]{0,.5,1},1); + house(world,62,"oak",new double[]{0,.5,1.5},0); + for(int wood=0;wood<6;wood++) for(int pattern=0;pattern<2;pattern++) { + int x=wood*6;for(int y=0;y<2;y++) { + WindowBlock block=ArchitectureBlocks.findWindow(ArchitectureBlocks.WOODS[wood],1,true,pattern==0,pattern==1); + world.setBlockState(new BlockPos(x+pattern*2,64+y,23),block.getDefaultState(),2); + } + } + BlockPos border=new BlockPos(15,64,27);world.getChunkFromBlockCoords(border.east()); + for(int y=0;y<2;y++) { + world.setBlockState(border.up(y),ArchitectureBlocks.findWindow("oak",0,false,true,false).getDefaultState().withProperty(WindowBlock.FACING,EnumFacing.EAST),2); + world.setBlockState(border.south(2).up(y),ArchitectureBlocks.findWindow("oak",1,true,false,true).getDefaultState().withProperty(WindowBlock.FACING,EnumFacing.EAST),2); + } + if(!WindowController.set(world,WindowController.column(world,border),true,null,null,false)) throw new IllegalStateException("Border preview could not open"); + } + private static void house(World world,int x,String wood,double[] offsets,int finish) { + for(int storey=0;storey=.5;WindowBlock window=ArchitectureBlocks.findWindow(wood,finish,true,storey%2==0,column==1,back); + world.setBlockState(new BlockPos(x+2+column,base+1+y,z),window.getDefaultState().withProperty(WindowBlock.FACING,EnumFacing.NORTH),2); + } + if(storey>0) WindowController.set(world,WindowController.column(world,new BlockPos(x+3,base+1,z)),true,null,null,false); + } + } +} diff --git a/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/WindowProbe.java b/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/WindowProbe.java new file mode 100644 index 0000000..b49e3a1 --- /dev/null +++ b/src/probe/java/zone/moddev/mc/ironagearchitecture/probe/WindowProbe.java @@ -0,0 +1,170 @@ +package zone.moddev.mc.ironagearchitecture.probe; + +import java.util.*; +import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; +import net.minecraft.entity.item.EntityItem; +import net.minecraft.entity.item.EntityArmorStand; +import net.minecraft.init.Blocks; +import net.minecraft.item.*; +import net.minecraft.util.*; +import net.minecraft.util.math.*; +import net.minecraft.world.WorldServer; +import net.minecraftforge.common.util.*; +import zone.moddev.mc.ironagearchitecture.ArchitectureBlocks; +import zone.moddev.mc.ironagearchitecture.block.*; + +final class WindowProbe { + private static void require(boolean ok,String message){if(!ok) throw new IllegalStateException(message);} + static void verify(WorldServer world,boolean reload) { + int metadata=0,operations=0;BlockPos pos=new BlockPos(15,90,-48); + // A border arena needs the diagonal chunks too: power queries deliberately never load them. + for(int x=-1;x<=1;x++) for(int z=-1;z<=1;z++) world.getChunkFromChunkCoords((pos.getX()>>4)+x,(pos.getZ()>>4)+z); + FakePlayer player=FakePlayerFactory.getMinecraft(world);player.capabilities.isCreativeMode=false;player.setHeldItem(EnumHand.MAIN_HAND,null);player.setPosition(8,90,-60); + for(WindowBlock block:ArchitectureBlocks.WINDOWS) { + require(Item.getItemFromBlock(block.canonical())!=null,"Missing canonical window item"); + if(block.right||block.back) require(Item.getItemFromBlock(block)==null,"Hidden window has an item"); + for(int meta=0;meta<16;meta++) { + IBlockState state=block.getStateFromMeta(meta);require(block.getMetaFromState(state)==meta,"Window metadata "+state); + for(Mirror mirror:Mirror.values()) {IBlockState once=block.withMirror(state,mirror);require(once.getBlock().withMirror(once,mirror).equals(state),"Window mirror "+state);} + IBlockState rotated=state;for(int i=0;i<4;i++) rotated=block.withRotation(rotated,Rotation.CLOCKWISE_90);require(rotated.equals(state),"Window rotation");metadata++; + } + for(EnumFacing facing:EnumFacing.HORIZONTALS) { + clear(world,pos);IBlockState state=block.getDefaultState().withProperty(WindowBlock.FACING,facing); + world.setBlockState(pos,state,2);world.setBlockState(pos.up(),state,2); + Vec3d start=point(pos,8,8,-16,facing),end=point(pos,8,8,32,facing); + require(block.collisionRayTrace(state,world,pos,start,end)!=null,"Closed pane targeting "+state); + BlockPos leaf=pos.offset(facing);world.setBlockState(leaf.up(),Blocks.STONE.getDefaultState(),3); + require(!WindowController.toggle(world,pos,null)&&!world.getBlockState(pos).getValue(WindowBlock.OPEN)&&!world.getBlockState(pos.up()).getValue(WindowBlock.OPEN),"Partial opening on obstruction"); + world.setBlockToAir(leaf.up()); + AxisAlignedBB swing=WindowGeometry.sweep(block.right,facing,block.back).offset(pos); + EntityArmorStand entity=new EntityArmorStand(world,(swing.minX+swing.maxX)/2,pos.getY(),(swing.minZ+swing.maxZ)/2);world.spawnEntity(entity); + require(!WindowController.toggle(world,pos,null),"Opened through an entity");world.removeEntityDangerously(entity); + require(WindowController.toggle(world,pos,null)&&world.getBlockState(pos.up()).getValue(WindowBlock.OPEN),"Column did not open"); + require(WindowController.owns(world,leaf,pos)&&WindowController.owns(world,leaf.up(),pos.up()),"Missing outside cells"); + IBlockState opened=world.getBlockState(pos); + require(block.collisionRayTrace(opened,world,pos,start,end)==null,"Open aperture still targets a pane"); + List proxyBoxes=WindowGeometry.boxes(block.thin,block.right,true,facing,WindowGeometry.UP,true,block.back); + AxisAlignedBB proxyBox=proxyBoxes.get(proxyBoxes.size()-1).offset(leaf);double cx=(proxyBox.minX+proxyBox.maxX)/2,cy=(proxyBox.minY+proxyBox.maxY)/2,cz=(proxyBox.minZ+proxyBox.maxZ)/2; + Vec3d a=facing.getAxis()==EnumFacing.Axis.Z?new Vec3d(cx-1,cy,cz):new Vec3d(cx,cy,cz-1),b=facing.getAxis()==EnumFacing.Axis.Z?new Vec3d(cx+1,cy,cz):new Vec3d(cx,cy,cz+1); + require(ArchitectureBlocks.WINDOW_LEAF.collisionRayTrace(world.getBlockState(leaf),world,leaf,a,b)!=null,"Outside leaf cannot be targeted"); + for(double z=-.8;z<1.8;z+=.2) { + Vec3d centre=point(pos,8,1.5,z*16,facing); + AxisAlignedBB body=new AxisAlignedBB(centre.xCoord-.3,centre.yCoord,centre.zCoord-.3,centre.xCoord+.3,centre.yCoord+1.8,centre.zCoord+.3); + require(world.getCollisionBoxes(player,body).isEmpty(),"Player cannot fit through open column "+state); + } + require(ArchitectureBlocks.WINDOW_LEAF.getDrops(world,leaf,world.getBlockState(leaf),0).isEmpty(),"Independent helper drops"); + require(ArchitectureBlocks.WINDOW_LEAF.getPickBlock(world.getBlockState(leaf),null,world,leaf,player).getItem()==Item.getItemFromBlock(block.canonical()),"Helper pick identity"); + require(WindowController.toggle(world,pos.up(),null)&&world.isAirBlock(leaf)&&world.isAirBlock(leaf.up()),"Column did not close"); + world.setBlockState(pos.down(),Blocks.REDSTONE_BLOCK.getDefaultState(),3); + require(world.getBlockState(pos).getValue(WindowBlock.OPEN)&&world.getBlockState(pos.up()).getValue(WindowBlock.POWERED),"Power at lower section "+state+" actual="+world.getBlockState(pos)+" power="+world.isBlockPowered(pos)); + require(WindowController.toggle(world,pos,null)&&!world.getBlockState(pos).getValue(WindowBlock.OPEN),"Manual powered override"); + world.setBlockState(pos.offset(facing.getOpposite()),Blocks.STONE.getDefaultState(),3); + require(!world.getBlockState(pos).getValue(WindowBlock.OPEN),"Unchanged power erased manual override"); + world.setBlockToAir(pos.offset(facing.getOpposite()));world.setBlockToAir(pos.down()); + world.setBlockState(leaf,Blocks.STONE.getDefaultState(),3);world.setBlockState(pos.up(2),Blocks.REDSTONE_BLOCK.getDefaultState(),3); + require(!world.getBlockState(pos).getValue(WindowBlock.OPEN),"Blocked redstone opening");world.setBlockToAir(leaf); + require(world.getBlockState(pos).getValue(WindowBlock.OPEN)&&world.getBlockState(pos).getValue(WindowBlock.POWERED),"Power did not retry after clearing obstruction"); + world.setBlockToAir(pos.up(2));require(!world.getBlockState(pos).getValue(WindowBlock.OPEN),"Falling power did not close"); + WindowController.toggle(world,pos,null);world.setBlockState(leaf,Blocks.STONE.getDefaultState(),3); + require(!world.getBlockState(pos).getValue(WindowBlock.OPEN)&&world.getBlockState(leaf).getBlock()==Blocks.STONE,"Forced replacement deleted a foreign block"); + clear(world,pos);operations++; + } + } + // Mining the outside portion has exactly the owner's ordinary/creative drop behaviour. + WindowBlock sample=ArchitectureBlocks.findWindow("oak",1,true,true,true,true); + for(boolean creative:new boolean[]{false,true}) { + clear(world,pos);removeDrops(world,pos);player.capabilities.isCreativeMode=creative; + world.setBlockState(pos,sample.getDefaultState(),2);WindowController.toggle(world,pos,null);BlockPos leaf=pos.north();IBlockState proxy=world.getBlockState(leaf); + require(ArchitectureBlocks.WINDOW_LEAF.removedByPlayer(proxy,world,leaf,player,true),"Could not mine open leaf"); + ArchitectureBlocks.WINDOW_LEAF.harvestBlock(world,player,leaf,proxy,null,null); + List drops=world.getEntitiesWithinAABB(EntityItem.class,new AxisAlignedBB(pos.add(-2,-1,-2),pos.add(3,3,3))); + require(drops.size()==(creative?0:1),"Duplicate/lost helper harvest drops "+drops.size()); + require(world.isAirBlock(pos)&&world.isAirBlock(leaf),"Harvest left a window/helper");removeDrops(world,pos); + } + player.capabilities.isCreativeMode=false; + placement(world,pos,player); + clear(world,pos);world.setBlockState(pos,sample.getDefaultState(),2); + AxisAlignedBB swept=WindowGeometry.sweep(sample.right,EnumFacing.NORTH,sample.back).offset(pos); + EntityArmorStand blocker=new EntityArmorStand(world,(swept.minX+swept.maxX)/2,pos.getY(),(swept.minZ+swept.maxZ)/2);world.spawnEntity(blocker); + world.setBlockState(pos.down(),Blocks.REDSTONE_BLOCK.getDefaultState(),3); + require(!world.getBlockState(pos).getValue(WindowBlock.OPEN),"Powered window opened through entity"); + world.removeEntityDangerously(blocker);WindowController.retryEntities(world); + require(world.getBlockState(pos).getValue(WindowBlock.OPEN),"Entity-blocked power edge never retried"); + clear(world,pos);world.setBlockState(pos,sample.getDefaultState(),2); + blocker=new EntityArmorStand(world,(swept.minX+swept.maxX)/2,pos.getY(),(swept.minZ+swept.maxZ)/2);world.spawnEntity(blocker); + world.setBlockState(pos.down(),Blocks.REDSTONE_BLOCK.getDefaultState(),3);world.removeEntityDangerously(blocker); + require(WindowController.toggle(world,pos,null)&&WindowController.toggle(world,pos,null),"Manual override of a pending power edge"); + WindowController.retryEntities(world);require(!world.getBlockState(pos).getValue(WindowBlock.OPEN),"Pending retry erased a manual override"); + clear(world,pos);removeDrops(world,pos); + world.setBlockState(pos,sample.getDefaultState(),2);WindowController.toggle(world,pos,null); + ArchitectureBlocks.WINDOW_LEAF.onBlockExploded(world,pos.north(),new net.minecraft.world.Explosion(world,null,pos.getX(),pos.getY(),pos.getZ(),1,false,true)); + require(world.isAirBlock(pos)&&world.isAirBlock(pos.north()),"Explosion left owner/helper"); + require(world.getEntitiesWithinAABB(EntityItem.class,new AxisAlignedBB(pos.add(-2,-1,-2),pos.add(3,3,3))).size()==1,"Explosion duplicated/lost window");removeDrops(world,pos); + // Reconcile links on either side without replacing unrelated blocks. + world.setBlockState(pos.north(),ArchitectureBlocks.WINDOW_LEAF.getDefaultState().withProperty(WindowLeafBlock.FACING,EnumFacing.SOUTH),2); + WindowController.repair(world,pos.north());require(world.isAirBlock(pos.north()),"Orphan helper remained"); + world.setBlockState(pos,sample.getDefaultState(),2);WindowController.toggle(world,pos,null); + world.setBlockState(pos.north(),net.minecraft.init.Blocks.AIR.getDefaultState(),2); // break callback closes the owner + world.setBlockState(pos,world.getBlockState(pos).withProperty(WindowBlock.OPEN,true),2); + WindowController.repair(world,pos);require(WindowController.owns(world,pos.north(),pos),"Missing helper was not repaired");clear(world,pos); + // Save all visible choices, both hinge directions and both thin half-cell placements. + int saved=0; + for(WindowBlock block:ArchitectureBlocks.WINDOWS) { + BlockPos at=new BlockPos(-112+saved*3,75,160);EnumFacing facing=EnumFacing.getHorizontal(saved&3); + boolean open=(saved&1)!=0,power=(saved&2)!=0;IBlockState expected=block.getDefaultState().withProperty(WindowBlock.FACING,facing).withProperty(WindowBlock.OPEN,open).withProperty(WindowBlock.POWERED,power); + if(reload) {require(world.getBlockState(at).equals(expected)&&world.getBlockState(at.up()).equals(expected),"Window reload "+saved);require(WindowController.owns(world,at.offset(facing),at)==open,"Helper reload "+saved);} + else { + world.setBlockState(at,block.getDefaultState().withProperty(WindowBlock.FACING,facing),2);world.setBlockState(at.up(),block.getDefaultState().withProperty(WindowBlock.FACING,facing),2); + if(power) world.setBlockState(at.down(),Blocks.REDSTONE_BLOCK.getDefaultState(),3); + require(WindowController.set(world,WindowController.column(world,at),open,power,null,false),"Saved window setup "+saved); + } + saved++; + } + System.out.println("IAA_WINDOW_PROBE_PASS metadata="+metadata+" directionalColumns="+operations+" savedColumns="+saved+" reload="+reload); + } + private static Vec3d point(BlockPos pos,double x,double y,double z,EnumFacing facing){double[] p=WallGeometry.rotate(x,y,z,facing);return new Vec3d(pos.getX()+p[0]/16,pos.getY()+p[1]/16,pos.getZ()+p[2]/16);} + private static void placement(WorldServer world,BlockPos pos,FakePlayer player) { + for(boolean thin:new boolean[]{false,true}) for(EnumFacing facing:EnumFacing.HORIZONTALS) for(boolean right:new boolean[]{false,true}) for(boolean rear:new boolean[]{false,true}) { + if(!thin&&rear) continue; + clear(world,pos);player.setSneaking(false);player.rotationYaw=facing.getOpposite().getHorizontalIndex()*90; + WindowBlock block=ArchitectureBlocks.findWindow("oak",0,thin,true,false);ItemStack stack=new ItemStack(block,4);player.setHeldItem(EnumHand.MAIN_HAND,stack); + double[] hit=WallGeometry.rotate(right?12:4,16,rear?12:4,facing); + require(stack.getItem().onItemUse(stack,player,world,pos,EnumHand.MAIN_HAND,EnumFacing.UP,(float)hit[0]/16,1,(float)hit[2]/16)==EnumActionResult.SUCCESS,"Initial placement"); + IBlockState placed=world.getBlockState(pos);WindowBlock actual=(WindowBlock)placed.getBlock(); + require(placed.getValue(WindowBlock.FACING)==facing&&actual.right==right&&actual.back==rear&&stack.stackSize==3,"Placement facing/hinge "+facing+" "+placed); + require(!actual.onBlockActivated(world,pos,placed,player,EnumHand.MAIN_HAND,stack,EnumFacing.UP,.5F,1,.5F),"Window item toggles clicked window"); + require(stack.getItem().onItemUse(stack,player,world,pos,EnumHand.MAIN_HAND,EnumFacing.UP,.5F,1,.5F)==EnumActionResult.SUCCESS,"Stack placement"); + require(world.getBlockState(pos.up()).getBlock()==actual&&world.getBlockState(pos.up()).getValue(WindowBlock.FACING)==facing,"Stack lost alignment"); + player.setHeldItem(EnumHand.MAIN_HAND,null);WindowController.toggle(world,pos,null); + BlockPos adjacent=pos.offset(facing.rotateY());world.setBlockState(adjacent,placed,2); + require(!world.getBlockState(adjacent).getValue(WindowBlock.OPEN),"Adjacent column opened together"); + WindowController.toggle(world,adjacent,null);WindowController.toggle(world,pos,null); + require(world.getBlockState(adjacent).getValue(WindowBlock.OPEN),"Adjacent column closed together");world.setBlockToAir(adjacent); + // A missing middle section must not silently merge incompatible open states. + world.setBlockToAir(pos.up());world.setBlockState(pos.up(2),placed,2);WindowController.toggle(world,pos.up(2),null); + ItemStack bridge=new ItemStack(block);player.setHeldItem(EnumHand.MAIN_HAND,bridge); + require(bridge.getItem().onItemUse(bridge,player,world,pos,EnumHand.MAIN_HAND,EnumFacing.UP,.5F,1,.5F)==EnumActionResult.FAIL&&bridge.stackSize==1&&world.isAirBlock(pos.up()),"Open/closed bridge was accepted"); + clear(world,pos);player.setHeldItem(EnumHand.MAIN_HAND,null); + } + for(EnumFacing facing:EnumFacing.HORIZONTALS) for(boolean interior:new boolean[]{false,true}) { + clear(world,pos);PartialWallBlock wall=ArchitectureBlocks.findPartial("oak",FrameFamily.POST,WallProfile.THIN); + IBlockState support=wall.getDefaultState().withProperty(FrameBlock.FACING,facing);world.setBlockState(pos.down(),support,2); + Vec3d standing=point(pos,8,0,interior?48:-32,facing);player.setPosition(standing.xCoord,standing.yCoord,standing.zCoord);player.rotationYaw=facing.getOpposite().getHorizontalIndex()*90; + ItemStack stack=new ItemStack(ArchitectureBlocks.findWindow("oak",0,true,true,false)); + require(stack.getItem().onItemUse(stack,player,world,pos.down(),EnumHand.MAIN_HAND,EnumFacing.UP,.5F,1,.5F)==EnumActionResult.SUCCESS,"Thin wall placement"); + IBlockState result=world.getBlockState(pos);WindowBlock actual=(WindowBlock)result.getBlock(); + require(result.getValue(WindowBlock.FACING)==(interior?facing.getOpposite():facing)&&actual.back==interior,"Thin wall outward facing "+result); + require(WallRuns.of(result,pos).get(0).aligned(WallRuns.of(support,pos.down()).get(0)),"Thin window moved off the wall plane"); + player.setSneaking(true);player.rotationYaw=facing.getHorizontalIndex()*90; + ItemStack override=new ItemStack(ArchitectureBlocks.findWindow("oak",0,false,true,false)); + require(override.getItem().onItemUse(override,player,world,pos,EnumHand.MAIN_HAND,EnumFacing.UP,.5F,1,.5F)==EnumActionResult.SUCCESS,"Sneak placement"); + require(world.getBlockState(pos.up()).getValue(WindowBlock.FACING)==facing.getOpposite(),"Sneaking did not override alignment"); + player.setSneaking(false);clear(world,pos); + } + player.setPosition(8,90,-60); + player.setSneaking(false); + } + private static void removeDrops(WorldServer world,BlockPos pos){for(EntityItem item:world.getEntitiesWithinAABB(EntityItem.class,new AxisAlignedBB(pos.add(-2,-1,-2),pos.add(3,3,3)))) world.removeEntityDangerously(item);} + private static void clear(WorldServer world,BlockPos pos){for(int y=-1;y<=3;y++) {world.setBlockToAir(pos.up(y));for(EnumFacing facing:EnumFacing.HORIZONTALS) world.setBlockToAir(pos.up(y).offset(facing));}} +} diff --git a/src/test/java/zone/moddev/mc/ironagearchitecture/EraSelectionTest.java b/src/test/java/zone/moddev/mc/ironagearchitecture/EraSelectionTest.java new file mode 100644 index 0000000..087593b --- /dev/null +++ b/src/test/java/zone/moddev/mc/ironagearchitecture/EraSelectionTest.java @@ -0,0 +1,24 @@ +package zone.moddev.mc.ironagearchitecture; + +import java.util.EnumSet; +import org.junit.Test; +import static org.junit.Assert.*; + +public class EraSelectionTest { + @Test public void defaultAllIncludesEveryEra() {assertEquals(EnumSet.allOf(Era.class),EraSelection.parse("all").allowed);} + @Test public void oneOrMoreIdsAreAllowed() { + assertEquals(EnumSet.of(Era.IRON_AGE),EraSelection.parse("iron_age").allowed); + assertEquals(EnumSet.allOf(Era.class),EraSelection.parse("iron_age","late_medieval_tudor").allowed); + } + @Test public void sharedMaterialsNeedOnlyOneAllowedEra() { + assertTrue(EraSelection.parse("iron_age").permits(EnumSet.allOf(Era.class))); + assertFalse(EraSelection.parse("iron_age").permits(EnumSet.of(Era.LATE_MEDIEVAL_TUDOR))); + } + @Test public void emptySelectionDisablesRecipes() {assertFalse(EraSelection.parse().permits(EnumSet.allOf(Era.class)));} + @Test public void invalidIdsNeverEnableForbiddenEras() { + EraSelection selection=EraSelection.parse("unknown-era");assertTrue(selection.allowed.isEmpty());assertEquals(1,selection.unknown.size()); + } + @Test public void whitespaceCaseAndDuplicatesAreHarmless() { + assertEquals(EnumSet.of(Era.IRON_AGE),EraSelection.parse(" IRON_AGE ","iron_age","").allowed); + } +} diff --git a/src/test/java/zone/moddev/mc/ironagearchitecture/HorizontalRailTest.java b/src/test/java/zone/moddev/mc/ironagearchitecture/HorizontalRailTest.java new file mode 100644 index 0000000..692ad86 --- /dev/null +++ b/src/test/java/zone/moddev/mc/ironagearchitecture/HorizontalRailTest.java @@ -0,0 +1,42 @@ +package zone.moddev.mc.ironagearchitecture; + +import java.util.*; +import org.junit.Test; +import static org.junit.Assert.*; +import net.minecraft.util.EnumFacing; +import zone.moddev.mc.ironagearchitecture.block.*; +import zone.moddev.mc.ironagearchitecture.block.WallGeometry.*; + +public class HorizontalRailTest { + private int count(List parts,Grain grain){int count=0;for(Part part:parts) if(part.grain==grain) count++;return count;} + @Test public void cornerRailsFollowOnlyTheirConnectedLeg() { + for(WallProfile profile:WallProfile.values()) if(profile.corner) for(EnumFacing facing:EnumFacing.HORIZONTALS) for(int height=0;height<2;height++) { + int shift=height==0?4:8; + List alongX=PartialWallGeometry.parts(profile,FrameFamily.TERMINAL,0,facing,WallGeometry.bit(facing.rotateY())< alongZ=PartialWallGeometry.parts(profile,FrameFamily.TERMINAL,0,facing,WallGeometry.bit(facing.getOpposite())< parts=PartialWallGeometry.parts(WallProfile.CORNER_L,FrameFamily.TERMINAL,0,facing,incoming); + assertEquals(0,count(parts,Grain.EAST_WEST));assertEquals(0,count(parts,Grain.NORTH_SOUTH)); + } + } + @Test public void rearCornerRailsStartAtTheInsidePostRatherThanShowingThroughTheOtherWall() { + for(WallProfile profile:WallProfile.values()) if(profile.corner) for(int pattern=0;pattern<2;pattern++) { + List parts=PartialWallGeometry.parts(profile,FrameFamily.TERMINAL,pattern,EnumFacing.NORTH,WallGeometry.bit(EnumFacing.EAST)<<4); + double start=Double.POSITIVE_INFINITY; + for(Part part:parts) if(part.grain==Grain.EAST_WEST&&part.front>profile.z0+4) for(double[] point:part.outline) start=Math.min(start,point[0]); + assertEquals(profile.x0+8-(pattern==0?1.5:3),start,1e-8); + List across=PartialWallGeometry.parts(profile,FrameFamily.TERMINAL,pattern,EnumFacing.NORTH,WallGeometry.bit(EnumFacing.SOUTH)<<4); + double z=16;for(Part part:across) if(part.grain==Grain.NORTH_SOUTH&&part.outline[0][0]>profile.x0+4) z=Math.min(z,part.front); + assertEquals(profile.z0+8-(pattern==0?1.5:3),z,1e-8); + } + List full=WallGeometry.parts(FrameFamily.TERMINAL,0,EnumFacing.NORTH,WallGeometry.bit(EnumFacing.EAST)<<4); + double start=16;for(Part part:full) if(part.grain==Grain.EAST_WEST&&part.front>8) for(double[] point:part.outline) start=Math.min(start,point[0]); + assertEquals(13,start,1e-8); + } +} diff --git a/src/test/java/zone/moddev/mc/ironagearchitecture/JettyGeometryTest.java b/src/test/java/zone/moddev/mc/ironagearchitecture/JettyGeometryTest.java index e9e6e7e..43bf74d 100644 --- a/src/test/java/zone/moddev/mc/ironagearchitecture/JettyGeometryTest.java +++ b/src/test/java/zone/moddev/mc/ironagearchitecture/JettyGeometryTest.java @@ -9,6 +9,23 @@ import zone.moddev.mc.ironagearchitecture.block.WallGeometry.*; public class JettyGeometryTest { + @Test public void bracketFacesPointOutOfTheSolidInsteadOfIntoIt() { + for(FloorShape shape:FloorShape.values()) for(boolean full:new boolean[]{false,true}) for(EnumFacing side:EnumFacing.HORIZONTALS) { + if(JettyFloorGeometry.bracketAnchor(shape,side,full)==null) continue; + List parts=JettyFloorGeometry.parts(shape,false,0,side,full); + Part bracket=parts.get(parts.size()-1); + double[] centre=new double[3]; + for(double[] p:bracket.outline) {centre[0]+=p[0]/bracket.outline.length;centre[1]+=p[1]/bracket.outline.length;} + centre[2]=(bracket.front+bracket.back)/2; + for(SurfaceGeometry.Face face:SurfaceGeometry.faces(Collections.singletonList(bracket),EnumFacing.NORTH)) { + double[] a=face.points[0],b=face.points[1],c=face.points[2]; + double nx=(b[1]-a[1])*(c[2]-a[2])-(b[2]-a[2])*(c[1]-a[1]); + double ny=(b[2]-a[2])*(c[0]-a[0])-(b[0]-a[0])*(c[2]-a[2]); + double nz=(b[0]-a[0])*(c[1]-a[1])-(b[1]-a[1])*(c[0]-a[0]); + assertTrue("Inward bracket face: "+shape+" "+side+" full="+full,nx*(a[0]-centre[0])+ny*(a[1]-centre[1])+nz*(a[2]-centre[2])>1e-6); + } + } + } @Test public void allWallPartsStayInsideTheirSavedFootprint() { for(WallProfile p:WallProfile.values()) for(FrameFamily f:FrameFamily.values()) { if(p.corner&&f!=FrameFamily.TERMINAL) continue; @@ -79,4 +96,17 @@ public class JettyGeometryTest { assertTrue("Corner tie missing at "+x+", "+z,found); } } + @Test public void floorClosesTheWallHeadWithoutFillingTheCantileverAir() { + List parts=JettyFloorGeometry.parts(FloorShape.FULL,false,3<<16,null,false); + boolean closed=false,overhangFilled=false; + for(Part part:parts) { + AxisAlignedBB box=part.bounds(EnumFacing.NORTH); + if(box.minY<=.25&&box.maxY>=.25&&box.minX<=.25&&box.maxX>=.25) { + if(box.minZ<=.25&&box.maxZ>=.25) closed=true; + if(box.minZ<=.75&&box.maxZ>=.75) overhangFilled=true; + } + } + assertTrue("Half-block opening above the lower wall",closed); + assertFalse("Empty space beyond the supporting wall must remain open",overhangFilled); + } } diff --git a/src/test/java/zone/moddev/mc/ironagearchitecture/SurfaceGeometryTest.java b/src/test/java/zone/moddev/mc/ironagearchitecture/SurfaceGeometryTest.java new file mode 100644 index 0000000..ad4330c --- /dev/null +++ b/src/test/java/zone/moddev/mc/ironagearchitecture/SurfaceGeometryTest.java @@ -0,0 +1,63 @@ +package zone.moddev.mc.ironagearchitecture; + +import java.util.*; +import org.junit.Test; +import static org.junit.Assert.*; +import net.minecraft.util.EnumFacing; +import zone.moddev.mc.ironagearchitecture.block.*; +import zone.moddev.mc.ironagearchitecture.block.WallGeometry.*; + +public class SurfaceGeometryTest { + private static double area(List parts) { + double total=0;for(SurfaceGeometry.Face face:SurfaceGeometry.faces(parts,EnumFacing.NORTH)) total+=SurfaceGeometry.area(face.points);return total; + } + @Test public void identicalBoxesHaveOneOutsideSkin() { + List parts=new ArrayList<>(); + WallGeometry.box(parts,0,0,0,16,16,16,Grain.DAUB); + WallGeometry.box(parts,0,0,0,16,16,16,Grain.VERTICAL); + assertEquals(6*256,area(parts),1e-4); + } + @Test public void adjacentBoxesHideTheirInternalFaces() { + List parts=new ArrayList<>(); + WallGeometry.box(parts,0,0,0,8,16,16,Grain.EAST_WEST); + WallGeometry.box(parts,8,0,0,16,16,16,Grain.NORTH_SOUTH); + assertEquals(6*256,area(parts),1e-4); + } + @Test public void overlappingRailsAndPostsDoNotDoubleTheirTopSurface() { + List parts=new ArrayList<>(); + WallGeometry.box(parts,6,0,0,10,16,8,Grain.VERTICAL); + WallGeometry.box(parts,0,13,0,16,16,2,Grain.EAST_WEST); + double top=0; + for(SurfaceGeometry.Face face:SurfaceGeometry.faces(parts,EnumFacing.NORTH)) { + boolean upper=true;for(double[] p:face.points) if(Math.abs(p[1]-16)>1e-6) upper=false; + if(upper) top+=SurfaceGeometry.area(face.points); + } + assertEquals(4*8+16*2-4*2,top,1e-4); + } + @Test public void separatedBoxesKeepBothSkins() { + List parts=new ArrayList<>();WallGeometry.box(parts,0,0,0,4,4,4,Grain.DAUB);WallGeometry.box(parts,8,8,8,12,12,12,Grain.DAUB); + assertEquals(2*6*16,area(parts),1e-4); + } + @Test public void clippingPreservesBoundsAndVisibleTimberInAllFacings() { + for(EnumFacing facing:EnumFacing.HORIZONTALS) { + List parts=PartialWallGeometry.parts(WallProfile.CORNER_L,FrameFamily.TERMINAL,0,facing,0xff0); + List faces=SurfaceGeometry.faces(parts,facing);assertFalse(faces.isEmpty()); + boolean wood=false; + for(SurfaceGeometry.Face face:faces) { + if(face.part.grain!=Grain.DAUB) wood=true; + for(double[] p:face.points) { + double[] q=face.part.transform(p[0],p[1],p[2],facing); + for(double coordinate:q) assertTrue(coordinate>=-1e-5&&coordinate<=16.00001); + } + } + assertTrue(wood); + } + } + @Test public void inverseTransformHandlesHorizontalAndTurnedParts() { + Part part=new Part(new double[][]{{0,0},{16,0},{16,16},{0,16}},8,10,Grain.RISING,EnumFacing.WEST,true); + for(EnumFacing facing:EnumFacing.HORIZONTALS) { + double[] p=part.transform(3,5,9,facing),q=part.inverse(p[0],p[1],p[2],facing); + assertArrayEquals(new double[]{3,5,9},q,1e-9); + } + } +} diff --git a/src/test/java/zone/moddev/mc/ironagearchitecture/TimberCornerTest.java b/src/test/java/zone/moddev/mc/ironagearchitecture/TimberCornerTest.java new file mode 100644 index 0000000..decc65c --- /dev/null +++ b/src/test/java/zone/moddev/mc/ironagearchitecture/TimberCornerTest.java @@ -0,0 +1,38 @@ +package zone.moddev.mc.ironagearchitecture; + +import java.util.*; +import org.junit.Test; +import static org.junit.Assert.*; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.AxisAlignedBB; +import zone.moddev.mc.ironagearchitecture.block.*; +import zone.moddev.mc.ironagearchitecture.block.WallGeometry.Part; + +public class TimberCornerTest { + @Test public void cornerContinuationsKeepTheirWidthWoodAndCellBounds() { + for(boolean thin:new boolean[]{false,true}) for(int corner=0;corner<4;corner++) for(int wood=0;wood<6;wood++) { + List parts=TimberCorners.add(Collections.emptyList(),thin,(wood+1)<<(corner*3));assertEquals(2,parts.size()); + for(Part part:parts) { + assertEquals(ArchitectureBlocks.WOODS[wood],part.timber); + double[][] polygon=new double[3][];for(int i=0;i<3;i++) polygon[i]=new double[]{part.outline[i][0],part.outline[i][1],0}; + assertEquals(1.25*1.25/2,SurfaceGeometry.area(polygon),1e-8); + for(EnumFacing facing:EnumFacing.HORIZONTALS) {AxisAlignedBB b=part.bounds(facing);assertTrue(b.minX>=0&&b.minY>=0&&b.minZ>=0&&b.maxX<=1&&b.maxY<=1&&b.maxZ<=1);} + } + } + } + @Test public void continuationFacesHaveOutwardWinding() { + for(int corner=0;corner<4;corner++) for(Part part:TimberCorners.add(Collections.emptyList(),true,1<<(corner*3))) { + double x=0,y=0;for(double[] p:part.outline){x+=p[0]/3;y+=p[1]/3;} + double[] centre={x,y,(part.front+part.back)/2}; + for(SurfaceGeometry.Face face:SurfaceGeometry.faces(Collections.singletonList(part),EnumFacing.NORTH)) { + double[] a=face.points[0],b=face.points[1],c=face.points[2]; + double nx=(b[1]-a[1])*(c[2]-a[2])-(b[2]-a[2])*(c[1]-a[1]),ny=(b[2]-a[2])*(c[0]-a[0])-(b[0]-a[0])*(c[2]-a[2]),nz=(b[0]-a[0])*(c[1]-a[1])-(b[1]-a[1])*(c[0]-a[0]); + assertTrue(nx*(a[0]-centre[0])+ny*(a[1]-centre[1])+nz*(a[2]-centre[2])>1e-8); + } + } + } + @Test public void unconnectedInfillKeepsItsOriginalGeometry() { + List plain=WallGeometry.parts(FrameFamily.INFILL,0,EnumFacing.NORTH,0); + assertSame(plain,TimberCorners.add(plain,true,0)); + } +} diff --git a/src/test/java/zone/moddev/mc/ironagearchitecture/TimberJoinTest.java b/src/test/java/zone/moddev/mc/ironagearchitecture/TimberJoinTest.java new file mode 100644 index 0000000..143e0b1 --- /dev/null +++ b/src/test/java/zone/moddev/mc/ironagearchitecture/TimberJoinTest.java @@ -0,0 +1,44 @@ +package zone.moddev.mc.ironagearchitecture; + +import java.util.*; +import org.junit.Test; +import static org.junit.Assert.*; +import net.minecraft.util.EnumFacing; +import zone.moddev.mc.ironagearchitecture.block.*; +import zone.moddev.mc.ironagearchitecture.block.WallGeometry.*; + +public class TimberJoinTest { + private List parts(FrameFamily family,int pattern,WallProfile profile,int mask) { + return profile==null?WallGeometry.parts(family,pattern,EnumFacing.NORTH,mask):PartialWallGeometry.parts(profile,family,pattern,EnumFacing.NORTH,mask); + } + private double edge(List parts,boolean left) { + double result=left?Double.NEGATIVE_INFINITY:Double.POSITIVE_INFINITY; + for(Part part:parts) if(part.grain==Grain.EAST_WEST) for(double[] p:part.outline) result=left?Math.max(result,p[0]):Math.min(result,p[0]); + return result; + } + @Test public void oneSidedRailsStopAtStudsRatherThanTheEmptyEdgeOfThePanel() { + for(WallProfile profile:new WallProfile[]{null,WallProfile.THIN,WallProfile.SHORT_LEFT,WallProfile.SHORT_RIGHT}) for(int pattern=0;pattern<2;pattern++) for(boolean left:new boolean[]{false,true}) { + if(profile==WallProfile.SHORT_LEFT&&!left||profile==WallProfile.SHORT_RIGHT&&left) continue; + double shift=profile==null||profile==WallProfile.THIN?0:profile==WallProfile.SHORT_LEFT?-4:4; + double expected=(pattern==0?(left?9:7):(left?11.5:4.5))+shift; + int mask=WallGeometry.bit(left?EnumFacing.WEST:EnumFacing.EAST)<<4; + assertEquals("profile="+profile+" pattern="+pattern+" left="+left,expected,edge(parts(FrameFamily.STUD,pattern,profile,mask),left),1e-8); + } + } + @Test public void oneSidedRailsFinishOnTheDiagonalAtTheirOwnHeight() { + for(int pattern=0;pattern<2;pattern++) for(boolean left:new boolean[]{false,true}) for(boolean top:new boolean[]{false,true}) { + boolean near=(pattern==0)!=top; + double expected=left?(near?4.25:16):(near?0:11.75); + int mask=WallGeometry.bit(left?EnumFacing.WEST:EnumFacing.EAST)<<(top?8:4); + assertEquals(expected,edge(parts(FrameFamily.BRACE,pattern,WallProfile.THIN,mask),left),1e-8); + } + } + @Test public void completeRailRunsAndDeliberateRailPanelsKeepTheirWidth() { + for(FrameFamily family:new FrameFamily[]{FrameFamily.POST,FrameFamily.STUD,FrameFamily.BRACE,FrameFamily.TERMINAL}) { + List both=parts(family,1,WallProfile.THIN,(WallGeometry.bit(EnumFacing.WEST)|WallGeometry.bit(EnumFacing.EAST))<<4); + assertEquals(16,edge(both,true),1e-8);assertEquals(0,edge(both,false),1e-8); + } + assertEquals(16,edge(parts(FrameFamily.RAIL,0,WallProfile.THIN,0),true),1e-8); + assertEquals(0,edge(parts(FrameFamily.RAIL,0,WallProfile.THIN,0),false),1e-8); + } +} diff --git a/src/test/java/zone/moddev/mc/ironagearchitecture/WindowGeometryTest.java b/src/test/java/zone/moddev/mc/ironagearchitecture/WindowGeometryTest.java new file mode 100644 index 0000000..418efa6 --- /dev/null +++ b/src/test/java/zone/moddev/mc/ironagearchitecture/WindowGeometryTest.java @@ -0,0 +1,53 @@ +package zone.moddev.mc.ironagearchitecture; + +import java.util.*; +import org.junit.Test; +import static org.junit.Assert.*; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.AxisAlignedBB; +import zone.moddev.mc.ironagearchitecture.block.*; + +public class WindowGeometryTest { + private double area(List faces){double a=0;for(WindowGeometry.Face f:faces) a+=SurfaceGeometry.area(f.vertices);return a;} + @Test public void movingAndSplittingPreserveTheSurface() { + for(boolean thin:new boolean[]{false,true}) for(boolean diamond:new boolean[]{false,true}) for(boolean right:new boolean[]{false,true}) for(EnumFacing facing:EnumFacing.HORIZONTALS) for(int joins:new int[]{0,3,15}) { + double closed=area(WindowGeometry.faces(thin,diamond,right,false,facing,joins,false)); + double open=area(WindowGeometry.faces(thin,diamond,right,true,facing,joins,false))+area(WindowGeometry.faces(thin,diamond,right,true,facing,joins,true)); + assertEquals(closed,open,1e-4); + } + } + @Test public void everyCellMeshAndCollisionBoxStaysInsideItsCell() { + for(boolean open:new boolean[]{false,true}) for(boolean right:new boolean[]{false,true}) for(EnumFacing facing:EnumFacing.HORIZONTALS) for(boolean proxy:new boolean[]{false,true}) { + for(WindowGeometry.Face face:WindowGeometry.faces(true,true,right,open,facing,3,proxy)) for(double[] p:face.vertices) { + for(int i=0;i<3;i++) assertTrue(Arrays.toString(p),p[i]>=-1e-6&&p[i]<=16.000001); + assertTrue(p[3]>=-1e-6&&p[3]<=16.000001);assertTrue(p[4]>=-1e-6&&p[4]<=16.000001); + } + for(AxisAlignedBB b:WindowGeometry.boxes(true,right,open,facing,3,proxy)) assertTrue(b.minX>=0&&b.minY>=0&&b.minZ>=0&&b.maxX<=1&&b.maxY<=1&&b.maxZ<=1); + } + } + @Test public void openCasementGivesAPlayerRoomThroughTwoStackedSections() { + for(boolean thin:new boolean[]{false,true}) for(boolean right:new boolean[]{false,true}) for(double z=-.8;z<1.5;z+=.1) { + AxisAlignedBB player=new AxisAlignedBB(.2,1.5/16,z-.3,.8,1.5/16+1.8,z+.3); + for(int y=0;y<2;y++) for(boolean proxy:new boolean[]{false,true}) for(AxisAlignedBB box:WindowGeometry.boxes(thin,right,true,EnumFacing.NORTH,y==0?WindowGeometry.UP:WindowGeometry.DOWN,proxy)) + assertFalse(box.offset(0,y,proxy?-1:0).intersectsWith(player)); + } + } + @Test public void closedGlassBlocksTheCentreAndOnlyOpenLeavesUseOutsideCells() { + AxisAlignedBB path=new AxisAlignedBB(.4,.4,-.5,.6,.6,.5);boolean hit=false; + for(AxisAlignedBB b:WindowGeometry.boxes(false,false,false,EnumFacing.NORTH,0,false)) hit|=b.intersectsWith(path); + assertTrue(hit);assertTrue(WindowGeometry.boxes(false,false,false,EnumFacing.NORTH,0,true).isEmpty()); + assertFalse(WindowGeometry.boxes(false,false,true,EnumFacing.NORTH,0,true).isEmpty()); + } + @Test public void rearHalfPlacementsKeepTheirPlaneAndStillOpenOutwards() { + for(boolean right:new boolean[]{false,true}) for(EnumFacing facing:EnumFacing.HORIZONTALS) { + List closed=WindowGeometry.faces(true,true,right,false,facing,0,false,true); + List opened=WindowGeometry.faces(true,true,right,true,facing,0,false,true); + List outside=WindowGeometry.faces(true,true,right,true,facing,0,true,true); + assertFalse(outside.isEmpty());assertEquals(area(closed),area(opened)+area(outside),1e-4); + for(WindowGeometry.Face face:closed) for(double[] vertex:face.vertices) { + EnumFacing inverse=facing==EnumFacing.EAST?EnumFacing.WEST:facing==EnumFacing.WEST?EnumFacing.EAST:facing; + assertTrue(WallGeometry.rotate(vertex[0],vertex[1],vertex[2],inverse)[2]>=8-1e-6); + } + } + } +}