Compare commits

...
1167 Commits
Author SHA1 Message Date
piexel 59626d2434 Add light theme
Follows the system by default.
2026-10-19 23:01:31 +02:00
piexel 46f5472081 Use IntelliJ icons in view list
Adds flatlaf-extras for SVG icons.
2026-10-19 23:01:31 +02:00
piexel 2cd9167549 Polish map view list
Outline icons, keycaps, row hover.
2026-10-19 22:46:49 +02:00
piexel a648ef6622 Build object meshes from a block grid instead of a BlockVector map
Every face looked up its neighbour in a HashMap keyed by BlockVector,
whose hash codes collide heavily; on the woodland mansion that took 1.8 s
of a 2.4 s build, so making a block see-through lagged by seconds. The
blocks now go into an array over the object's bounds, which brings the
build down to about 50-70 ms with the same faces. Opening an object gets
the same speed-up.

Model used: Claude Opus 5.5
2026-10-19 21:52:11 +02:00
piexel 51c9d01bf7 Follow the layer slider live and keep a ghost of what it cuts away
Cutting layers no longer rebuilds the mesh: it is built once with the top
faces hidden by the block above kept aside, so a cut is a quick filter on
the UI thread and the view follows the slider while it is dragged. The
cut-away part stays as a faint ghost that only fills pixels the shown part
leaves empty, so the object's shape stays readable without hazing over
the floor being looked into.

Model used: Claude Opus 5.5
2026-10-19 21:46:52 +02:00
piexel 99258bfd98 Pin folder headers, cut layers and see through blocks in the Objects view
Folder headers stay pinned at the top of the object tree while scrolling,
one per nesting level; clicking one jumps to that folder. A faint slider
over the 3D view (or Page Up/Down) cuts away the top layers so floors of
large structures like the woodland mansion can be looked into. Clicking a
block in the overview list makes that block type see-through, so whatever
it hides shows behind it.

Model used: Claude Opus 5.5
2026-10-19 21:39:42 +02:00
piexel db3d42df26 Keep every view in sight above the explorer sidebar
The divider between the view list and an explorer's sidebar could sit
or be dragged above the end of the list, squeezing it into a small
scrolling area. The list's full height is now the divider's lowest
limit: it starts there and can only be dragged further down.

Model used: Claude Opus 5.5
2026-10-19 21:06:16 +02:00
piexel b97bd51f20 Draw every block in the Objects view the way Minecraft models it
The 3D object view guessed block shapes: stairs, gates, lanterns and
similar became full cubes, fences and walls showed only their post,
glass fell back to grey, and vines and ladders became crossed plants.
It now reads the real models: every matching multipart part, each box
with its own texture, UVs and face rotation, tilted and rescaled boxes,
blockstate turns with uvlock, cullfaces against covering neighbours, and
Minecraft's side shading.

Chests, heads, banners and the bell use their entity textures; fluids
and other code-drawn blocks get simple shapes. See-through textures like
water, ice and stained glass are blended back to front. Texture objects
with a "sprite" key (glass in 26.x) are read, which also fixes them in
the palette map, and grey PNGs such as stone are no longer brightened by
Java's colour conversion.

Model used: Claude Opus 5.5
2026-10-19 20:59:41 +02:00
piexel a02b4386a4 Add an Objects view to the studio map
Inspired by the Procedural Objects workbench, the map window gets an
Objects view (key O) to browse every object in the pack:

- The sidebar lists the objects as a folder tree, with a search that
  matches every typed word anywhere in an object's path.
- The object is drawn in 3D with Minecraft's textures by a small
  software renderer: an orthographic camera, a depth buffer and only
  the block sides that can be seen. Drag orbits, shift-drag or the
  middle button pans, scrolling zooms and R resets. Plants are drawn
  as crossed sprites, single box models like slabs and carpets keep
  their shape, grass and leaves get plains' colours, and the object
  is centred beside the floating sidebar. A 150,000 block tree draws
  in about 55 ms.
- The right panel shows the object's size, block count, block types
  and how much of its box it fills, with a filterable list of its
  blocks by share.

BlockTextures can now resolve a block's texture on every side through
its blockstate rotation, sharing the model reading with the top
texture lookup.

Model used: Claude Opus 5.5
2026-10-19 20:29:53 +02:00
piexel 5a99091193 Remove the generation load views from the map
The Objects, Decorators and Layers load views coloured each biome by a
rough count from its file: object density times chance, decorator
chance and the number of palette layers. They measured nothing and the
Palettes view shows the same contents in detail, so they go, together
with their render types, biome colours and the engine's per-biome
maximums that only they used.

Model used: Claude Opus 5.5
2026-10-19 20:17:47 +02:00
piexel 88c2688662 Let the view list and explorer sidebar be resized vertically
The divider between the map's view list and the palette or noise
sidebar was one pixel tall, too thin to grab. It is now a 7 px band
with a grip that can be dragged up or down. Palettes and Noise share
the one split, so the divider stays put when switching between them.
Its starting height is measured once the sidebar is laid out.

Model used: Claude Opus 5.5
2026-10-19 19:58:41 +02:00
piexel f73c78349b Show the noise explorer as a view in the studio map
/iris std noise now opens the map window on a new Noise view under
Generators (key N), next to Palettes. The noise sources sit below the
view list in the sidebar island, and the seed and colour controls join
the map toolbar. Reset view (R) resets whichever explorer is showing.

/iris std explore still opens its own noise window for one generator.

Model used: Claude Opus 5.5
2026-10-19 19:53:07 +02:00
piexel 0a76208194 Float the sidebar island over the map
The map now fills the whole window area and the sidebar island floats
over it, with the map showing through the gap around the island. Clicks
in that gap reach the map. Dragging the island's right edge resizes it,
always leaving the map some room.

Model used: Claude Opus 5.5
2026-10-19 18:21:27 +02:00
piexel 38b340ac14 Make the map sidebar a resizable island
Long palette names were cut off by the sidebar's fixed width. The
sidebar is now a rounded island set apart from the window edges, and
dragging the gap beside it makes it wider or narrower.

Model used: Claude Opus 5.5
2026-10-19 18:19:35 +02:00
piexel bfeb017037 Let two block tall plants stand up on the decorator map
Squeezing both halves of tall grass into one block made it look like
short grass. The bottom half now fills its own block and the top half
covers the block above it on the map, so tall plants are two blocks tall
there too. The legend shows them by their top half, like Minecraft's
inventory icon.

Model used: Claude Opus 5.5
2026-10-19 18:16:06 +02:00
piexel 89b864a142 Show both halves of two block tall plants in decorator previews
A decorator places a two-part plant like tall grass as a bottom half with
its top half above it, but the preview only drew the bottom half. It now
squeezes both halves into the one block, and the inspector says so.

Model used: Claude Opus 5.5
2026-10-19 18:12:46 +02:00
piexel a49a4e0949 Stop dimming bare ground and draw layered plant sprites
Bare ground under a decorator preview was dimmed, which made the block
under each decoration look lit up. It now shows at its own brightness;
the sprites alone mark where the decorator places.

Plants whose model stacks several sprites, like the firefly bush with
its glowing fireflies, now draw every layer in order instead of only
the particle texture.

Model used: Claude Opus 5.5
2026-10-19 17:48:02 +02:00
piexel 3c773e3e57 Draw the biome's ground under decorator previews
Decorator previews used the dark "nothing here" colour around and
between their sprites. They now show the biome's top surface layer
there: through the see-through parts of each sprite, and dimmed where
the decorator places nothing or places air. Hovering a bare spot says
the decorator places nothing there.

Model used: Claude Opus 5.5
2026-10-19 17:26:46 +02:00
piexel 84ffce263a Show decorator sprites and list snippets in the palette explorer
Decorators now draw their plant sprite on each block instead of its
average colour, which only suits stacked ground layers.

The Overview tab lists every decorator and block palette snippet with
how often biomes use it. Picking one opens it where the first biome
places it; its rows open each other use. Snippet files no biome uses yet
are listed too. Snippet names keep their folders, so two snippets with
the same file name in different folders stay apart.

Model used: Claude Opus 5.5
2026-10-19 17:21:01 +02:00
piexel e4675ecab8 Show biome decorators in the studio palette explorer
Each decorator of a biome is now listed and previewed next to its layers,
walls and slabs. The preview shows where the decorator places, using the
same seed as the generator, and which block it picks there. Notes explain
what it leaves out: the part it decorates, competition with other
decorators, stacking and height.

Palettes that come from a snippet file show the snippet name, read from
the biome file since snippets are copied in when the pack loads.

Model used: Claude Opus 5.5
2026-10-19 17:15:12 +02:00
piexel b24c38896d List each region biome's palettes in the Here tab
The Here tab listed palettes only for the biome you stand on. The other
biomes in your region, including child biomes nested under their parent,
showed just their name, so a child like Grassland Hills had no way to
reach its wall blocks from there. Every biome in the region now lists its
palettes under it, indented with the biome.

Model used: Claude Opus 5.5
2026-10-19 14:37:27 +02:00
piexel 6270b62552 Revert the 3D noise carving
Carving rock behind cliff faces with noise didn't give mountains the
look it was meant to, so it comes out again: the noiseCarving biome
setting, its mantle component and the NOISE_CARVED flag.

This reverts 8c39f9a01 and 8a1c7ac99.

Model used: Claude Opus 5.5
2026-10-19 14:24:28 +02:00
piexel 8a1c7ac998 Stop noise carving from stalling new worlds
Noise carving ran inside the cave carving pass, which visits every
chunk within cave range of the chunk being generated, and it looked up
the biome and height of every column in all of them. Opening a studio
froze the server for about 35 seconds on its first chunk, even for
packs that don't use noise carving.

It now has its own mantle component that runs last and only for the
chunk being generated, and it does nothing when no biome in the pack
uses it. A fresh studio's spawn chunk is ready in about 1.4 seconds
again.

Model used: Claude Opus 5.5
2026-10-19 14:22:07 +02:00
piexel 8c39f9a01b Sculpt overhangs and arches into terrain with 3D noise carving
Iris terrain is one height per column, so mountains could never lean
out over the ground below. Biomes can now set noiseCarving, which
removes rock behind cliff faces and slopes wherever a 3D noise drops
under a threshold. The hollows open up into overhangs, arches through
ridges and ragged cliffs instead of hiding inside the mountain.

Gentle ground keeps a crust of rock so trees and grass keep their
footing, carving fades out toward the biome's borders, and nothing is
carved at or below sea level. The carved blocks go through the normal
carve pass, so they get cave walls, floors, ceilings and decoration.

Model used: Claude Opus 5.5
2026-10-19 14:14:18 +02:00
piexel 74756dd19e Remove the before and after hotload comparison from the map
The single before picture wasn't pulling its weight. The toolbar
buttons, the C shortcut and the swipe and changes overlays are gone
until there's a better way to compare pack versions.

Model used: Claude Opus 5.5
2026-10-19 13:52:04 +02:00
piexel 5363cbbc02 Show every palette a biome places, and where child biomes grow
Palettes listed only a biome's surface layers. They now include sea
layers, cave ceiling layers, and wall and slab blocks, each previewed
with the noise and random source the generator uses for that kind.
Wall and slab blocks also change with height, so their preview is drawn
at sea level and says so.

The Here tab names the parent of a child biome and lists the region's
biomes with their children nested under them, marking where you stand.

Model used: Claude Opus 5.5
2026-10-19 13:31:45 +02:00
piexel 19bbca0e4b Fit the height colours to the terrain on screen
The height view spread its colours over the whole world height, so a
mountain range and the plains next to it got nearly the same colour.
The legend now measures the visible heights (1st to 99th percentile,
rounded to 8 blocks) and fits the gradient to that range, with sea level
kept as the water/land split.

The map also re-finds its own world after a reload instead of whichever
Iris world is active, now that the active world prefers the open studio.

Model used: Claude Opus 5.5
2026-10-19 13:31:45 +02:00
piexel ec0d2d816a Keep the map legend readable
Long biome names now shorten instead of pushing the percentages out of
the legend, and biomes that share a display name show their key. The
highlight edge tints instead of drawing solid white, which turned
biomes scattered pixel by pixel into noise.

Model used: Claude Opus 5.5
2026-10-19 12:51:58 +02:00
piexel 340e66105c Show the pack you are working on in the noise explorer
/iris std noise listed the generators of whichever Iris world Bukkit
listed first. A normal Iris world runs on its own copy of the pack from
when it was created, so generators added to the pack since (like a new
generators/2026 folder) were missing while you worked in the studio.

The noise commands now use the world you stand in, and otherwise the
open studio before any other Iris world. After a hotload the explorer
stays on the world it was opened for.

Model used: Claude Opus 5.5
2026-10-19 12:49:56 +02:00
piexel e58d065ca4 Make the map explain what it shows
Highlight a biome or region (legend click, right-click, or H under the
cursor) to fade everything else and outline it; Esc clears it. A live
legend beside the map lists the biomes or regions in view with their
share of the screen.

The hover card now answers at once from the drawn tiles and then adds
height, sea level, land or sea, slope, cave biome and the Minecraft
biome; Shift adds keys and files.

After a refresh or hotload, C compares with the map from before: a
draggable before/now divider, or a view that fades out everything that
stayed the same and says how much changed.

Shading (S) adds hillshade relief to every view and Borders (B) outlines
the regions. Tiles now remember what each pixel shows, so all of this
restyles without rendering again.

Model used: Claude Opus 5.5
2026-10-19 12:46:35 +02:00
piexel cebd582018 Hand the display server one finished map picture per paint
The map drew every visible tile to the window on its own, hundreds of
scaled images per paint, and since tiles are no longer cached in the
display server each of those was uploaded again on every frame, which
made dragging and zooming lag. The tiles are now put together in memory
once, only when tiles or the view change, and drawn as a single image
like the palette view does.

Model used: Claude Opus 5.5
2026-10-19 11:18:59 +02:00
piexel f44ce5b327 Keep map tiles out of the display server's GPU memory
Opening the map crashed Xwayland inside Mesa's Intel driver, which took
the server, the game client and every other X window down with it.
Map tiles and noise frames were left for Java2D to copy into textures
inside the display server, hundreds of them for one map. They are now
drawn straight from memory again, like before the render rework.

Model used: Claude Opus 5.5
2026-10-19 11:14:37 +02:00
piexel b1445430bb Size the studio windows for high DPI screens
FlatLaf grows fonts and its own controls to the screen's DPI, but the
sizes the studio set itself stayed fixed, so on a 192 DPI screen text
was cut off and overlapped. Every padding, row height, column, card and
legend now grows with the same scale or follows the font, the windows
open at a scaled size that still fits the screen, and the theme is
installed before any window builds its controls. Long status text now
shortens with an ellipsis instead of pushing the rest of the bar away.

Model used: Claude Opus 5.5
2026-10-19 11:05:55 +02:00
piexel 80e7e37791 Put the palettes inside one modern, faster map window
/iris std map now opens a single window with a grouped list of views
(world, biome layers, generation load and the new Palettes view), each
with a number or letter key. The palette explorer lives in that window
instead of its own: double-click or right-click a spot on the map to see
its palettes, and switching views keeps both looking at the same place.
/iris std pal still works and opens the map straight on its palettes.

The map draws much faster. Tiles are cached per power-of-two zoom level
so zooming reuses them, coarser and finer tiles already in the cache
stand in while new ones render, renders start the moment you drag or
zoom, and tiles that are still on screen keep rendering instead of being
thrown away. The noise explorer keeps one worker pool instead of making
threads for every frame and skips work for frames that were replaced.
Pointer coordinates now live in a status bar, so moving the mouse no
longer redraws the whole map.

Model used: Claude Opus 5.5
2026-10-19 10:42:40 +02:00
piexel 8e71c14f65 Give the studio windows an IntelliJ style dark theme with FlatLaf
FlatLaf is loaded at runtime through slimjar like the other libraries.
Every studio window now opens with FlatLaf's dark look tuned to the
IntelliJ New UI colours, and shares a small kit of controls: a flat
toolbar, toolbar buttons and tabs, search boxes, a status bar and the
rounded cards drawn over maps.

Model used: Claude Opus 5.5
2026-10-19 10:42:35 +02:00
piexel eb1a244114 Let /iris find search the focus region a dimension is locked to
With focusRegion set the world only generates that region, but find
checked the dimension's region list, which the focus region does not
have to be part of, so everything in it was reported as missing.

Model used: Claude Opus 5.5
2026-10-18 22:46:04 +02:00
piexel 05eed1f6a5 Let /iris find look inside child biomes
Find biome, structure and object only checked the biomes a region lists
directly, so a biome that only appears as another biome's child was
reported as not being in any region.

Model used: Claude Opus 5.5
2026-10-18 22:43:59 +02:00
piexel 6c3822cdaf Find textures for blocks whose palette entry leaves properties at their defaults
Textures were looked up by the block state with only the properties the
pack set, so a plain grass_block never matched its snowy=false variant
and fell back to its map colour. The lookup now uses the full state.

Model used: Claude Opus 5.5
2026-10-18 22:20:41 +02:00
piexel 13312e9182 Show textures when they finish loading before the first palette layer
The explorer only reloaded the map for textures once a layer was already
showing. Now that the Minecraft jar is cached, textures can finish while
the first layer is still loading, which left that layer on map colours
until another layer was picked.

Model used: Claude Opus 5.5
2026-10-18 22:06:11 +02:00
piexel 674502eeda Colour grass, leaves and water in the palette explorer like Minecraft does
Every tinted block used one fixed plains green, so biomes with their own
grass colour looked nothing like the game. Tints now follow Minecraft's
own rules: its block tint table, the grass, foliage and dry foliage
colour maps from the client jar, vanilla biome colour overrides, the
dark forest and swamp grass modifiers with an exact copy of the swamp
noise, and the client's 5x5 biome blend. Custom biomes use the colours
Iris writes into their data pack, and the variant is picked per chunk
the same way Iris generates them.

Plants have no top face, so they now show their average colour instead
of their side-on sprite tiled into stripes.

Model used: Claude Opus 5.5
2026-10-18 21:59:35 +02:00
piexel 143ba4771b Make dragging the explorer map follow the mouse smoothly
While dragging or zooming, the map stood still until the mouse paused,
then restarted from a blocky draft. The last finished frame is now moved
and scaled with the view straight away, and a new frame starts as soon
as the previous one is done, sized from how fast the last frame drew so
cheap views come out sharp immediately.

Budgets are now measured in drawn pixels, so a frame where the layer is
missing everywhere no longer breaks the refinement step.

Model used: Claude Opus 5.5
2026-10-18 21:34:05 +02:00
piexel 02956fd358 Make the palette explorer open on the palette you are standing in
The explorer read the persistent world instead of the studio world the
player was in, and left out the studio focus region, so it showed the
wrong palettes. It now uses the player's own world, lists the focus
region first and opens on the surface biome centred on the player.

Blocks are sampled once per whole block and drawn at full resolution
without smoothing, so textures look like they do in game. The view and
depth controls use styled buttons instead of the default Swing widgets.

Model used: Claude Opus 5.5
2026-10-18 21:22:31 +02:00
piexel 76860e94fa List where you are in the palette explorer instead of following you
Biomes are 3D, so the biome at a player's position underground isn't the
surface biome above it. The Here tab now lists the region, the surface
biome and, when different, the biome at the player's position, each with
its layers, followed by the region's other biomes. It updates as the player
moves but no longer changes the selection, and biomes without a key no
longer crash it.

Model used: Claude Opus 5.5
2026-10-18 21:09:50 +02:00
piexel 252facf41e Show real block textures and a region overview in the palette explorer
Block textures come from the Minecraft client jar, downloaded once from
Mojang and cached under plugins/Iris/cache. Each block shows its tinted top
face: texture pixels when zoomed in, the texture's average colour when
zoomed out, and the map colour when no texture is available.

The sidebar now has two tabs. Here follows the player who opened it and
shows their region, the biome they stand in with its layers, and the
region's other biomes, opening each new biome they walk into. Overview is a
searchable tree of region, biome group, biome and layer.

Model used: Claude Opus 5.5
2026-10-18 20:33:32 +02:00
piexel ad88b00c2a Add a palette explorer for previewing biome palette layers
/iris studio palette (alias pal) opens the explorer on the biome you stand
in, listing each biome's palette layers instead of noise styles. The map
shows the blocks the layer places, the raw noise, or both, and a side panel
shows which noise values the layer produces, which value range each block
owns, and how its real share of the ground compares to its weight.

World generation and the preview now sample layers through the same
IrisBiome methods, so the preview matches what the world places.

Model used: Claude Opus 5.5
2026-10-18 20:12:00 +02:00
piexel 6f930d1370 Apply axialFracturing to a style's built-in fractures
axialFracturing on a style only changed how that style fractures its
parent, so setting it on a style like IRIS changed nothing. It now also
makes the style's own built-in fractures axial. A fracture the pack
adds keeps its own axialFracturing setting.

Model used: Claude Opus 5.5
2026-10-18 14:38:20 +02:00
piexel 855ab90be4 Stop fractured noise from drawing straight diagonal lines
Default fracturing pushed every sample by one value along the (1, -1)
diagonal, so heavily fractured styles such as FRACTAL_SMOKE and the
IRIS family formed straight 45 degree edges. It still takes a single
fracture sample, but the push direction now turns with that sample's
value. That costs a few multiplications per fracture level: 0% on
FRACTAL_SMOKE and FRACTAL_WATER, 4% on IRIS_DOUBLE.

axialFracturing read its second offset from the same noise with
swapped coordinates, which mirrors the warp across x = z. Each axis
now samples a far, uncorrelated part of the fracture noise instead.

Both change terrain for fractured styles, so existing worlds will show
seams at the border to newly generated chunks.

Model used: Claude Opus 5.5
2026-10-18 14:35:31 +02:00
piexel 8783fa09a5 Generate schemas for snippets nested in lists and maps
Snippet discovery only followed a field's raw type, so element types of
KList, KMap and arrays were never visited. Snippets that only appear in
collections, such as decorator, deposit and tree, got no schema and no
workspace mapping.

Model used: Claude Opus 5.5
2026-10-18 13:23:08 +02:00
piexel 87dc78934f Port the v4 noise explorer and Vision map, and fix blank windows on Wayland
Brings over v4's Noise Explorer and Vision viewers: a tiled, cancellable
renderer, viewport math, palettes, player and entity markers, and
click-to-teleport.

On Wayland desktops whose Xwayland window manager does not reparent
windows (niri, sway, Hyprland), Java's X11 toolkit waits for a reparent
that never comes, so it keeps its guessed window size and insets and
leaves most of the window white. Iris now sets
_JAVA_AWT_WM_NONREPARENTING natively before opening its first window
when the desktop needs it. GNOME and KDE are left alone because Java
already recognises their window managers.

All Iris windows, including the pregen view, now come from GuiHost, and
the studio commands report a headless server instead of failing.

Model used: Claude Opus 5.5
2026-10-15 14:42:28 +02:00
piexel d35fe93f7f Compile against Adventure 5.2.0, the version Paper 26.3 ships
Paper still provides Adventure at runtime. Every Adventure method Iris calls has the same signature in 4.17 and 5.2.0, so older servers keep working.

Model used: Claude Opus 5.5
2026-10-15 14:24:20 +02:00
piexel d6d6fd031c Size pregen backpressure from measured plates and post-GC heap
The heap gate and the engine service's keep-alive scaling read heap usage
right after the last collection instead of the moment's usage, which G1
lets climb near the maximum on a healthy heap. The plate budget now comes
from a measured 38 MB per plate on a 1088 block world instead of an
estimate that assumed 136 MB there, and is enforced as is rather than at
twice its value. Pregen evicts only the least recently used plates down to
the budget instead of every idle one, so it no longer reloads the plates
it is still generating around.

Model used: Claude Opus 5.5
2026-10-15 13:45:47 +02:00
piexel 7f34bb917f Hold pregen back when the mantle or the heap is full
Ported from Iris v4's mantle backpressure. Before each chunk the
pregenerator now:
- evicts idle tectonic plates and waits while more plates are loaded
  than the heap-scaled budget allows (pregen.maxResidentTectonicPlates)
- pauses between 92% and 82% heap, requesting a GC because a paused
  pregen allocates too little for the collector to run on its own; if
  the heap stays full, the JVM's diagnostic GC runs at most once a
  minute, which also works with -XX:+DisableExplicitGC
Both waits give up after pregen.mantleBackpressureTimeoutMs so a stuck
heap cannot hang the pregen. Unlike v4, a reclaim starts anywhere above
82% instead of only at 92%, since a pregen waiting at 89% otherwise
never triggers one.

Outside pregen, the engine service shortens the mantle keep-alive as
the heap fills and trims studio worlds once the heap is critical.

The mantle itself:
- concurrent requests for a plate that is still loading share one load
  instead of each holding an IO thread
- plate retries stop once the mantle is closed or the thread is
  interrupted, instead of recursing forever
- trim(0, 0) evicts plates idle for 500 ms instead of 4 s

Model used: Claude Opus 5.5
2026-10-15 00:26:33 +02:00
piexel 18e0834db8 Stop a failed pregen instead of leaving it running
An exception on the pregen thread ended the thread without shutting the
job down, so it kept reporting 0 chunks per second forever and blocked
starting another pregen. A failure is now logged and the job is closed.

Model used: Claude Opus 5.5
2026-10-15 00:26:26 +02:00
piexel 6a85cb53a9 Create mantle chunk flag locks only when a flag needs one
Every mantle chunk allocated a coroutine mutex for all 256 possible flag
ordinals up front, about 66 KB of locks per chunk, although only a few
flags are ever raised under a lock. During a pregen those locks took
well over a gigabyte of heap and ran 3 GB servers out of memory. Each
lock is now created when its flag is first raised.

Model used: Claude Opus 5.5
2026-10-15 00:26:21 +02:00
piexel 8a0fd9b9fc Support Minecraft 26.3
26.3 changed the chunk generator, biome source and structure placement
APIs, so it gets its own v26_3_R1 binding, compiled against the 26.3
dev bundle. v26_1_R1 keeps covering 26.1.2 and 26.2.

Binding changes against v26_1_R1:
- noise, surface and carvers run through a single buildTerrain stage
- the biome source hands out itself as the BiomeResolver
- structures are generated with a climate sampler, and starts are
  looked up per chunk instead of per section
- structure set salt and frequency come from the spreading placement
- biome colors are read from Vector3f attributes

Data pack format 121 with DataFixerV2603:
- biome mob spawns and creature spawn probability move into the
  natural_mob_spawns and creature_world_gen_spawn_probability attributes
  (26.3 silently ignores the old fields)
- the bed rule's "explodes" becomes "destroy_on_use", and straw beds get
  the same rule, matching vanilla

Model used: Claude Opus 5.5
2026-10-12 17:35:33 +02:00
piexel 2fc4c2faf9 Skip other versions' NMS bindings when preloading classes on disable
Older servers such as Paper 1.20.1 cannot read the Java 25 classes of
the 26.x binding and logged a conversion error for each of them on
shutdown. Bindings for other versions are never used, so they are not
loaded at all.

Model used: Claude Opus 5.5
2026-10-11 17:05:27 +02:00
piexel 475f72a300 Clean up leftover studio worlds in either world layout
Studio worlds live in the "iris" world folder, which on 26.x is inside
the level's dimensions folder rather than in the world container.

Model used: Claude Opus 5.5
2026-10-11 16:48:12 +02:00
piexel bc7a530486 Fix object block replacements on Paper 26.1.2
Paper 26.1.2 caches block data that names every state without the parsed
states BlockData#merge needs, so replacing blocks inside objects threw
on every placement. Such block data replaces the base completely, so
B.merge returns it directly.

Model used: Claude Opus 5.5
2026-10-11 16:48:12 +02:00
piexel b12b1ee304 Set up studio workspaces with Gradle 9.5.1
Gradle 8.14 cannot run on Java 25, which 26.x servers use. 9.5.1 runs on
both Java 21 and 25.

Model used: Claude Opus 5.5
2026-10-11 16:48:12 +02:00
piexel 196de7d858 Keep the Iris data pack loadable after upgrading to 26.1
26.1 requires has_ender_dragon_fight in dimension types and reads data
packs before any plugin runs, so a pack written on 1.21 stopped the
server from starting. 1.21 ignores the field, so it is now always
written.

Model used: Claude Opus 5.5
2026-10-11 16:48:12 +02:00
piexel 8a18b6ff8b Support Minecraft 26.1.2 and 26.2
Adds the v26_1_R1 binding, which covers both versions. 26.x servers are
not obfuscated and need Java 25, so the build moves to Gradle 9.5.1 and
paperweight-userdev 2.0.0-beta.21, and the binding compiles against the
26.1.2 dev bundle with a Java 25 toolchain while everything else stays on
Java 21.

On 26.x every world is a dimension inside the main level, so the binding
provides DimensionWorldStorage:
- world folders resolve to <level>/dimensions/<namespace>/<key>
- data packs go to the level's datapacks folder
- Iris data in a pre-26.1 world folder is moved into the dimension
  before Paper imports the world and deletes the old folder
- "main=true" swaps the world in as the level's overworld instead of
  changing level-name

Also:
- INMS compares 1.x and 26.x versions in release order
- data pack formats 101 (26.1.2) and 107 (26.2), declared with
  min_format/max_format from format 82 on
- dimension types get the fields 26.1 added
- the Java safeguard accepts Java 25

Model used: Claude Opus 5.5
2026-10-11 16:48:05 +02:00
piexel e1514503f6 Resolve world folders through a WorldStorage seam
Minecraft 26.1 moved every world into its level's dimensions folder, so
Iris can no longer assume <world container>/<world name>. World folders,
data pack folders and moving Iris data out of an old layout now go
through WorldStorage, which the NMS binding provides. Every binding so
far uses LegacyWorldStorage, so behaviour is unchanged.

Code that already holds a loaded World uses World#getWorldFolder, which
is correct for both layouts.

Model used: Claude Opus 5.5
2026-10-11 15:58:50 +02:00
piexel f1c1869e43 Require Paper and drop Spigot support
Core now compiles against paper-api instead of spigot-api. PaperLib is
replaced by Paper's native async chunk and teleport API, and the Spigot
fallbacks (Medieval pregen method, sync chunk loading) are removed.

Adventure is no longer bundled and relocated: Paper ships it and every
CommandSender is an Audience, so the BukkitAudiences wrapper and ShadeFix
go away.

Iris now refuses to enable on servers without the Paper API, with a clear
message instead of failing later at runtime.

Model used: Claude Opus 5.5
2026-10-11 15:06:37 +02:00
Julian Krings 31615dc052 improve craftengine data provider for normal blocks 2026-07-06 15:56:29 +02:00
Julian Krings 4ddfd9549f store custom blocks in mantle directly until chunk gen 2026-07-06 15:43:11 +02:00
Julian Krings ed67dd6058 fix not resolving block id properly for craftengine 2026-07-04 14:43:51 +02:00
Julian Krings ee7f54429a fix not finding matching provider in processUpdate phase 2026-07-04 14:43:07 +02:00
Julian Krings 1e0628f768 fix runServer tasks 2026-07-04 14:41:55 +02:00
Julian Krings 72faf76269 update to craftengine 26.5 2026-06-16 12:12:14 +02:00
Aidan Aeternum 28c4941941 v+ 2026-06-12 21:10:21 -04:00
Aidan Aeternum d452f10fd9 Merge pull request #1241 from VolmitSoftware/dev
3.9.2
2026-06-12 21:09:46 -04:00
Julian Krings e7d89cadd8 fix compile 2026-04-12 11:58:33 +02:00
Julian Krings cddd6b16d6 add CraftEngine data provider (#1227) 2026-04-12 11:55:36 +02:00
Julian Krings b2ac4e7f93 fix invalid json for ultrawarm dimensions on 1.21.11 2026-04-12 11:55:36 +02:00
Julian Krings c2bb32459f fix ambient particles breaking datapack generation on 1.21.11 2026-04-12 11:55:35 +02:00
Brian Neumann-Fopiano 41888e33f6 Fixing fallbacks for macos Folk
Compiles fine, just stricter defaults
2026-02-11 00:01:23 -05:00
Aidan Aeternum 25fa2553e5 v+ 2026-02-02 03:28:49 -05:00
Aidan Aeternum 86f78baecf Merge pull request #1236 from VolmitSoftware/dev
3.9.1
2026-02-02 03:28:30 -05:00
Julian Krings c31158578f fix datapack generation on 1.21.11 2026-01-29 16:40:33 +01:00
Aidan Aeternum 4e86d7d634 v+ 2026-01-27 20:09:25 -05:00
Aidan Aeternum 62fe29cf34 Merge pull request #1235 from VolmitSoftware/dev
3.9.0
2026-01-27 20:08:36 -05:00
Julian Krings a3bcea4f3e increase memory assumption per region 2026-01-26 21:37:29 +01:00
Julian Krings 7befce1084 initial 1.21.11 support 2026-01-23 11:46:15 +01:00
Aidan Aeternum 351a1aa495 v+ 2026-01-15 02:50:30 -05:00
Aidan Aeternum d2cb8a9032 Merge pull request #1234 from VolmitSoftware/dev
3.8.2
2026-01-15 02:50:03 -05:00
Julian Krings 509715087c fix typo 2026-01-13 15:11:21 +01:00
Julian Krings 12527ecdd8 improve cachedChunks lookup in the mantle writer 2026-01-05 14:58:58 +01:00
Julian Krings 01a2999e03 decrease locking for mantle generation 2026-01-05 14:58:09 +01:00
Julian Krings dbafe84fa5 make context dereference method fail-safe 2026-01-02 14:12:44 +01:00
Julian Krings 40b020fc5d improve matter generator once again 2026-01-02 14:10:35 +01:00
Julian Krings d15f7d62d1 cleanup async world ticker 2026-01-02 14:10:35 +01:00
Julian Krings f9c062c794 improve mantle cleanup to prevent thread starvation 2026-01-02 14:10:34 +01:00
Aidan Aeternum 6a89b8bd06 v+ 2026-01-02 06:17:00 -05:00
Aidan Aeternum 194fcb2ea7 Merge pull request #1232 from VolmitSoftware/dev
3.8.1
2026-01-02 06:16:28 -05:00
Julian Krings 3b68f855b2 get back some speed by loading four mantle regions into cache at once 2025-12-31 13:38:42 +01:00
Julian Krings cfbf68d37a add check for headless environments to the edit command 2025-12-31 13:01:18 +01:00
Julian Krings 9999f3e429 fix chunk artifacts 2025-12-31 13:00:45 +01:00
Julian Krings 97fa0562a4 fix custom block data 2025-12-23 22:51:41 +01:00
Julian Krings 3abe671851 suppress unchecked cast warning for the ChunkedDataCache 2025-12-18 18:53:59 +01:00
Julian Krings e164a3bb5c Merge branch 'feat/safeguard' into dev
# Conflicts:
#	build.gradle.kts
#	core/src/main/java/com/volmit/iris/Iris.java
2025-12-18 18:44:36 +01:00
Julian Krings 4dfb4441e4 fix buildscript 2025-12-18 14:23:51 +01:00
Julian Krings e686f67453 add command to fix offsets of all objects 2025-12-18 14:23:35 +01:00
Julian Krings e2eed4812a ignore empty objects for shrinkwrap 2025-12-18 14:22:40 +01:00
Julian Krings 1737833bfe fix legacy object parsing 2025-12-18 14:22:21 +01:00
piexel a4c5f46c37 Fix /iris object convert for .schem files with 128+ block types 2025-12-18 11:25:38 +01:00
piexel 025fa833c4 Fix Grass won't place on moss blocks 2025-12-18 11:21:16 +01:00
Julian Krings 98cc82cc3d Fix jigsaw editor bricking when opening a piece a missing object 2025-12-18 11:13:43 +01:00
Julian Krings 298365f588 fix studio open not working when not in spectator 2025-12-18 11:06:09 +01:00
Julian Krings 90e5720e2e improve buildscript 2025-12-18 11:05:28 +01:00
Julian Krings 7cd43791f4 fix offset with shrinkwrap 2025-12-18 11:05:14 +01:00
Julian Krings a251d192ad fix loot for objects underground 2025-12-18 11:00:04 +01:00
Julian Krings abfff28f43 fix redstone not rotating 2025-12-18 10:59:57 +01:00
Julian Krings fbdae4c928 improve overwriting of the regenerated chunks 2025-12-18 10:59:32 +01:00
Aidan Aeternum 93ca26e368 v+ 2025-12-17 12:12:25 -05:00
Aidan Aeternum 8c1db1c223 Merge pull request #1231 from VolmitSoftware/dev
3.8.0
2025-12-17 12:11:28 -05:00
Julian Krings 123708601f improve pregen cache 2025-12-01 16:01:54 +01:00
Julian Krings b5ab4968ba fix compile 2025-11-30 15:36:36 +01:00
Julian Krings 93ae6037bd whoops forgot to disable craftbukkit relocations on <=1.20.4 2025-11-29 11:49:15 +01:00
Julian Krings dd8e487a3b fix tile entity serialization on paper 1.21.6+ servers 2025-11-29 11:13:30 +01:00
Julian Krings d3c8377a12 allow switching between buildtools and userdev 2025-11-29 11:12:16 +01:00
Julian Krings 84f3687c0c update compat for iron bars 2025-11-27 16:09:31 +01:00
Julian Krings ed1e1f1181 whoops forgot those providers 2025-11-26 21:33:03 +01:00
Julian Krings 528c97f367 replace IrisCustomData class with a proxy to keep full access to the base BlockData 2025-11-26 21:32:13 +01:00
Julian Krings f7a459f3bc initial 1.21.9/10 bindings 2025-11-26 21:25:05 +01:00
Julian Krings cd179b4321 reintroduce native threads for the updater as a setting and cleanup 2025-11-26 20:55:48 +01:00
Julian Krings 6373dbb1b8 potential optimization for the noise cache 2025-11-21 16:21:54 +01:00
Julian Krings 0b0797f876 minor optimization for chunk updater 2025-11-21 16:21:31 +01:00
Julian Krings 446acefc91 minor optimization for the mantle writer 2025-11-21 16:21:02 +01:00
Julian Krings 7a44e555b2 rename noise cache size setting and decrease default value 2025-11-21 16:20:32 +01:00
Julian Krings 57d4c2935c add cache sizes to engine status 2025-11-21 16:16:15 +01:00
Julian Krings 234fb1b0c4 also include parent class for schema generation 2025-11-08 00:58:12 +01:00
Julian Krings 0882b5acc4 use parent shared classloader to reflect intelij behavior 2025-11-06 22:32:28 +01:00
Julian Krings 18da26e1fa minor performance improvement 2025-11-06 22:27:14 +01:00
Julian Krings 65aa95f2a5 fix wrong radius when marking mantle chunks as completed 2025-11-06 22:25:37 +01:00
Julian Krings 5330ddc4ec fix deleting object ids with mantle cleanup 2025-11-06 22:20:30 +01:00
Julian Krings a7fdd37569 fix studio loot command 2025-11-06 22:16:27 +01:00
Julian Krings a226fea9e2 fix regen command 2025-11-06 16:22:02 +01:00
Julian Krings 8cea165a29 even more performance improvements 2025-11-06 15:29:03 +01:00
Julian Krings 1d7cba184c fix linear palette not growing correctly 2025-11-06 14:41:39 +01:00
Julian Krings 4ca7ea3911 minor speed improvements 2025-11-01 22:36:25 +01:00
Julian Krings ea5919def2 stop writing first access each time the engine is opened 2025-10-30 16:40:35 +01:00
Julian Krings be35e49302 use coroutines for mantle generation 2025-10-30 16:40:08 +01:00
Julian Krings aadd03990a optimize data palette for mantle slices 2025-10-27 19:53:47 +01:00
Julian Krings 38a579453d optimize noise cache 2025-10-26 13:49:03 +01:00
Julian Krings 0bf5da2ca1 optimize object maps 2025-10-26 13:48:11 +01:00
Julian Krings 317848692e improve regen speed 2025-10-06 14:12:01 +02:00
Julian Krings 22118de9e9 fix object smart bore 2025-10-06 14:10:37 +02:00
Julian Krings d7039d120b fix place commands causing unwanted block updates 2025-10-06 13:00:00 +02:00
Julian Krings 979ee4e7d8 switch hashing algorithm for objects once more 2025-10-05 21:46:12 +02:00
Julian Krings f68d45bd30 dynamically resolve snippet classes 2025-10-05 00:21:38 +02:00
Julian Krings b86d7f303e restructure the shared kts classloader to be more consistent 2025-10-05 00:20:55 +02:00
Julian Krings c573843314 fix kts dependency resolver 2025-10-05 00:17:57 +02:00
Julian Krings 51a7bef18e whoops forgot escaping the path 2025-10-04 17:19:11 +02:00
Julian Krings 0e237aa1ad fix generated build.gradle.kts on external dives on windows 2025-10-04 13:41:48 +02:00
Julian Krings b46c413f6b make gradle setup print to console on failure 2025-10-04 13:40:47 +02:00
Julian Krings f3ef1ca2ae fix typo in preprocessors description 2025-10-04 13:40:07 +02:00
Julian Krings 703e61dd54 fix preprocessors not applying reliably 2025-10-04 13:39:33 +02:00
Aidan Aeternum e1ec6b7827 v+ 2025-10-03 16:08:05 -04:00
Aidan Aeternum f94292fdac Merge pull request #1228 from VolmitSoftware/dev
3.7.11
2025-10-03 16:07:47 -04:00
Julian Krings 7d153bf985 optimize objects to avoid hash collision 2025-10-02 15:25:37 +02:00
Julian Krings f85f15ed02 fix converter for complex schematic 2025-10-02 11:43:16 +02:00
Julian Krings 867686eced fix deserialisation of unversioned mantle plates 2025-10-01 12:57:38 +02:00
Julian Krings 526efd3ae1 restructure safeguard 2025-09-29 15:45:56 +02:00
Aidan Aeternum 9d796bd2a0 v+ 2025-09-27 07:34:58 -04:00
Aidan Aeternum 1a9a5d80ad Merge pull request #1223 from VolmitSoftware/dev
3.7.10
2025-09-27 07:34:31 -04:00
Julian Krings c5c7f9bdc5 fix minor issue with nms tools 2025-09-22 18:10:34 +02:00
Julian Krings 01a421b732 add file extensions to the script property descriptions 2025-09-21 22:35:34 +02:00
Julian Krings ae92bcf194 add kts hook for chunk updates 2025-09-21 22:30:48 +02:00
Julian Krings 7e7933858b suppress json syntax exceptions from being reported to sentry 2025-09-21 15:47:29 +02:00
Julian Krings 9c073ecbcb fix infinite loop due to writing the gradle.kts 2025-09-21 14:01:59 +02:00
Julian Krings f4617c1996 add tabcompletion for mythic mobs mob stacks 2025-09-21 12:51:59 +02:00
Julian Krings 21a2e4feef fix hotloading when changing kts 2025-09-21 12:13:57 +02:00
Julian Krings 258d0d3aaa make sure that the pack is installed correctly 2025-09-21 00:15:34 +02:00
Julian Krings 27b2fd0823 show other packs data again 2025-09-20 23:31:33 +02:00
Julian Krings 0524adb0df fix compile 2025-09-20 23:05:24 +02:00
Julian Krings 3981b0976d cleanup command framework and fix random locator fails 2025-09-20 23:05:16 +02:00
Julian Krings b5811cae08 cleanup IrisData usage 2025-09-20 23:04:11 +02:00
Julian Krings c998fd1fd9 move additional build data in its own constants class 2025-09-20 17:25:20 +02:00
Julian Krings a7d874d37f use scheduled thread pool for scoreboard svc to prevent freezes 2025-09-20 15:27:49 +02:00
Julian Krings 7c41f86fb3 add slope condition for slabs 2025-09-20 14:28:33 +02:00
Julian Krings e5e0561d5a cleanup world creator 2025-09-17 18:02:45 +02:00
Julian Krings d50cdfec3e cleanup pack installation 2025-09-17 18:02:20 +02:00
Julian Krings 00997c1902 return if missing datapack entries were found 2025-09-17 17:36:18 +02:00
Julian Krings 3095a92522 add experimental setting to force place custom blocks as early as possible 2025-09-10 23:06:46 +02:00
Julian Krings fca309dec7 move the loading block data message into debug 2025-09-10 17:02:51 +02:00
Julian Krings 2793ed1035 send block update after placing itemsadder blocks 2025-09-10 17:01:29 +02:00
Julian Krings e5908285af remove old worlds from cache before retrieving 2025-09-10 16:15:01 +02:00
Julian Krings a8ee321eb8 Allow placing datapack structures in Iris worlds (#1225)
* Allow placing datapack structures in Iris worlds

* command for generating configs for datapack structures

* remove the sub dir from the snippet key
2025-09-07 18:36:08 +02:00
Julian Krings aa14242b54 another mantle fix 2025-09-07 16:50:23 +02:00
Julian Krings f6968269b4 add toggle to offset noise types fixing seeds 2025-09-06 14:35:25 +02:00
Julian Krings 0d0251e2f1 add toggle to change rarity algorithm 2025-09-06 14:21:52 +02:00
Julian Krings 81b8fb02ae replace rarity calculation due to it breaking at more than two values 2025-09-06 14:21:52 +02:00
Julian Krings 77842489e5 add command for checking the effective rarity of regions 2025-09-06 14:21:52 +02:00
Julian Krings eda1f59d3a fix broken cave floors 2025-09-05 17:02:36 +02:00
Julian Krings 5418868559 fix hotloading mantle components 2025-09-05 16:56:56 +02:00
Julian Krings 1d81daafbb add option to control the noise threshold for cave shapes 2025-09-05 16:06:53 +02:00
Julian Krings 609a3585c1 use flat maven repository for classpath context 2025-09-05 16:04:49 +02:00
Julian Krings 571dde608c add shared class loader support for script dependencies 2025-09-05 12:21:28 +02:00
Julian Krings 3a13f5a7c1 fix dependency resolve failing on new intelij versions 2025-09-04 23:07:37 +02:00
Julian Krings e5654b74d4 Auto Completion for block properties (#1222) 2025-09-04 18:06:00 +02:00
Julian Krings a75738dd7a minor noise optimizations 2025-09-04 16:13:41 +02:00
Julian Krings 003be1f88b use mantle chunk for deposit modifier 2025-09-04 13:56:05 +02:00
Julian Krings 1eaafae20d move caffeine cache executors into a virtual thread executor 2025-09-04 13:55:34 +02:00
Julian Krings 79088c0305 fix hyperlock not releasing on exceptions 2025-09-04 13:55:03 +02:00
Julian Krings 9e147774bd revert mantle change to hopefully fix deadlock 2025-09-04 13:54:28 +02:00
Julian Krings e51b632c8f Merge pull request #1221 from VolmitSoftware/feat/kts
Kotlin script engine
2025-09-04 13:43:08 +02:00
Julian Krings 1aa64c9a02 add hook for custom engine modes 2025-09-03 17:02:39 +02:00
Julian Krings 1e148d8fcd minor code cleanup 2025-09-03 13:32:08 +02:00
Julian Krings f63cabd8b8 fix mantle flag serialization 2025-09-03 12:56:02 +02:00
Julian Krings 176d3a5f9f remove old system property 2025-09-02 22:14:59 +02:00
Julian Krings eb184983de fix json property description on vscode 2025-09-02 22:09:32 +02:00
Julian Krings d1c307865d properly generate gradle wrapper for kts tab completion 2025-09-02 22:05:11 +02:00
Julian Krings 4a26b8b34f fix compile 2025-09-02 21:57:34 +02:00
Julian Krings 33fd01c3ac Merge branch 'dev' into feat/kts
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/commands/CommandStudio.java
#	core/src/main/java/com/volmit/iris/engine/mantle/EngineMantle.java
#	core/src/main/java/com/volmit/iris/engine/mantle/components/MantleJigsawComponent.java
#	core/src/main/java/com/volmit/iris/util/mantle/Mantle.java
#	core/src/main/java/com/volmit/iris/util/mantle/MantleChunk.java
2025-09-02 12:47:23 +02:00
Julian Krings 34874080e7 cleanup some array types for json schema generation 2025-09-02 12:28:54 +02:00
Julian Krings 43131ed8f6 make jar scanner also load child classes 2025-09-02 12:27:26 +02:00
Julian Krings 9ea425aee4 isolate io stuff in its own MultiBurst 2025-09-02 12:27:01 +02:00
Aidan Aeternum 709f05c1a5 v+ 2025-08-29 18:23:28 -04:00
Aidan Aeternum ec74add5de Merge pull request #1219 from VolmitSoftware/dev
3.7.2
2025-08-29 18:23:02 -04:00
Julian Krings d5b706764a isolate jigsaw component from objects 2025-08-27 16:12:12 +02:00
Julian Krings ca4c205a4a decrease wait times in mantle components 2025-08-27 16:08:49 +02:00
Julian Krings 7b9c2ae6ad minor optimizations 2025-08-27 12:34:52 +02:00
Julian Krings d0e9d44152 fix random null pointer in the resource loader 2025-08-27 00:33:59 +02:00
Julian Krings 2cdffaae33 include stronghold in mantle radius calc 2025-08-27 00:33:30 +02:00
Julian Krings d4a8beac95 fix more structures not being placed properly 2025-08-27 00:32:58 +02:00
Julian Krings 0e0e4075d8 fix more unsafe mantle operations 2025-08-27 00:32:34 +02:00
Julian Krings 9c492a2e66 save dev command for mantle panics 2025-08-26 17:46:27 +02:00
Julian Krings 96bf83684c add the shutdown hook on plugin enable instead of disable to prevent issues 2025-08-26 17:45:20 +02:00
Julian Krings 25ea9ae62d make regen respect multicore setting 2025-08-26 17:23:49 +02:00
Julian Krings 7938c150dd fix the DataContainer for the last time 2025-08-26 17:23:33 +02:00
Julian Krings a7b4bf3ff2 hopefully fix the DataContainer this time 2025-08-25 20:53:52 +02:00
Julian Krings 693a05f2cb move the updater command out of dev 2025-08-25 19:44:03 +02:00
Julian Krings e48cbe1f69 performance optimizations and add two experimental options for further optimizations 2025-08-24 22:33:35 +02:00
Julian Krings 558f6fa8dd Merge pull request #1218 from VolmitSoftware/fix/mantle
Mantle Fixes
2025-08-24 22:24:09 +02:00
Julian Krings 67b29cc363 cleanup palette and fix incorrect bit resizing 2025-08-24 18:54:44 +02:00
Julian Krings 4702534f9c add size check to prevent further corruption in case of read offset 2025-08-24 17:21:21 +02:00
Julian Krings 29390c5e0a fix potential issues in the CountingDataInputStream 2025-08-24 17:20:01 +02:00
Julian Krings e8bfce469d change the trimming cycle to make sure one step happens per second 2025-08-24 17:19:50 +02:00
Julian Krings 770e2f47a2 centralize file channels to prevent concurrent access 2025-08-24 17:19:39 +02:00
Julian Krings 747e7b3330 optimize the chunk updater 2025-08-21 17:27:07 +02:00
Julian Krings 8662f3b47a update ItemsAdder namespaces on load data 2025-08-21 13:50:21 +02:00
Aidan Aeternum 6e738e69e7 v+ 2025-08-21 02:40:05 -04:00
Aidan Aeternum 4bf14c83e1 Merge pull request #1215 from VolmitSoftware/dev
3.7.1
2025-08-21 02:39:19 -04:00
Julian Krings 837674c295 update sentry link 2025-08-19 18:00:22 +02:00
Julian Krings e5f3bbd69e make ItemsAdderDataProvider more reliable when adding items & blocks 2025-08-12 22:18:56 +02:00
Julian Krings 2885a39299 cleanup script engine 2025-08-09 00:00:31 +02:00
Julian Krings bd1b06c761 make studio map zoom in on the cursor target 2025-08-08 21:40:36 +02:00
Julian Krings 3e1143112a fix offset in studio map and some other issues 2025-08-08 21:38:26 +02:00
Julian Krings 834b214fbf add setting to control deposits replacing bedrock 2025-08-07 17:11:38 +02:00
Julian Krings 768e569400 Merge branch 'dev' into feat/kts
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/project/SchemaBuilder.java
#	gradle/libs.versions.toml
2025-08-02 23:32:08 +02:00
Julian Krings a5d04333dd whoops messed up the structure slice 2025-08-02 23:19:11 +02:00
Julian Krings ebe8da0fd5 update slimjar for built-in spigot helper 2025-08-02 21:22:44 +02:00
Julian Krings 5eb25f3977 add property to use registry keys for where applicable
disabled by default for now
2025-08-02 00:01:33 +02:00
Julian Krings a5bca0a9bb fix not reading enum values correctly 2025-08-01 13:06:25 +02:00
Julian Krings a82882c1c1 move local maven repo into iris directory 2025-08-01 12:35:08 +02:00
Julian Krings 05193bd0d9 make depencency resolvers more predictable 2025-08-01 04:51:58 +02:00
Julian Krings 96efc15c36 fix local dependencies not resolving properly 2025-07-31 17:08:50 +02:00
Julian Krings 67f456cf53 cleanup script engine 2025-07-31 00:16:23 +02:00
Julian Krings 58b1bd115f update adventure api 2025-07-30 19:18:57 +02:00
Julian Krings 8ddc8abdb9 fix resolving dependencies in scripts 2025-07-30 16:54:19 +02:00
Julian Krings 12c2c71739 Merge branch 'refs/heads/dev' into feat/kts
# Conflicts:
#	core/build.gradle.kts
#	core/src/main/java/com/volmit/iris/Iris.java
#	core/src/main/java/com/volmit/iris/core/link/data/MythicMobsDataProvider.java
#	core/src/main/java/com/volmit/iris/util/misc/SlimJar.java
#	gradle/libs.versions.toml
2025-07-29 13:34:25 +02:00
Julian Krings 8705ca6e47 save structure placed at block 2025-07-28 21:44:27 +02:00
Julian Krings a610d0a7a9 remove unnecessary classpath injections 2025-07-28 20:53:01 +02:00
Julian Krings 0648cfd3fa add message for promotion of world to main world 2025-07-28 15:16:21 +02:00
Julian Krings fba9c17e3f fix not using relocated bukkit.yml correctly and remove duplicate code 2025-07-28 15:08:07 +02:00
Julian Krings 74128d58cf disable pack trimming for now 2025-07-28 15:06:34 +02:00
Julian Krings 70130e976d add getter method to IrisWorlds 2025-07-28 13:53:11 +02:00
Julian Krings 501c426302 fix slimjar error for new projects 2025-07-28 12:31:07 +02:00
Julian Krings 8471f15bc8 make world creation more failsafe 2025-07-27 23:23:04 +02:00
Julian Krings c4539441a0 fix datapack generation ignoring worlds when updating from <3.7.0 2025-07-27 23:22:40 +02:00
Julian Krings 472a98da16 update slimjar 2025-07-27 18:43:33 +02:00
Julian Krings 76a6f1465a update slimjar 2025-07-26 14:31:45 +02:00
Julian Krings b0ca0e3617 whoops messed up the extension retrieval 2025-07-25 13:21:16 +02:00
Julian Krings 08b9058c8f make git commit retrieval fail safe 2025-07-25 13:07:13 +02:00
Julian Krings 556bef6e43 fix more unsafe mantle chunk accesses 2025-07-25 12:44:01 +02:00
Julian Krings 16a4d20c90 use grgit to resolve current commit 2025-07-23 18:06:52 +02:00
Julian Krings e5f2fee926 add info about used commit to sentry reports 2025-07-23 15:07:06 +02:00
Julian Krings 3949468a60 inject into the library loader to make sure libraries can always be loaded 2025-07-23 14:45:54 +02:00
Julian Krings fc54fcb7eb make itemsadder data provider standalone 2025-07-22 12:12:13 +02:00
Julian Krings 9d6e4e87d8 fix custom blocks not placing 2025-07-22 12:11:41 +02:00
Julian Krings f50e964d4f hopefully fix unsafe mantle chunk operations 2025-07-21 17:14:45 +02:00
Julian Krings 8262e52893 cleanup regen 2025-07-19 18:10:57 +02:00
Julian Krings 1c41dc69ce Merge pull request #1213 from VolmitSoftware/feat/caves
Feat/caves
2025-07-19 17:59:32 +02:00
Julian Krings dee46a4284 cleanup object mask rotation 2025-07-19 17:48:58 +02:00
Julian Krings a8892b04ef bump nms for 1.21.8 support 2025-07-19 12:59:50 +02:00
Julian Krings d49f7d7821 add parameter to the create command to make it your main world 2025-07-13 17:43:11 +02:00
Julian Krings 106a3834ab fix iris create command failing 2025-07-13 17:42:10 +02:00
Julian Krings 387e8adfe2 ignore errors when scanning for external data providers 2025-07-12 18:31:39 +02:00
Julian Krings 9ade90d9ca make the mythic mobs link a data provider and make adding custom mob plugins easier 2025-07-12 15:51:49 +02:00
Julian Krings 3d2392843a update maven core to fix dependency resolution issues 2025-07-12 14:18:35 +02:00
Julian Krings a9891e819a v+ 2025-07-12 12:51:04 +02:00
Julian Krings 02c13a9391 Merge pull request #1211 from VolmitSoftware/dev
3.7.0
2025-07-12 12:42:38 +02:00
Julian Krings f6590c26e7 fix download failing for dimensions with snippets 2025-07-11 13:07:20 +02:00
Julian Krings 64bb81626c update overworld pack 2025-07-11 12:54:16 +02:00
Julian Krings 343dc429d5 fix null pointer when failing to load a dimension 2025-07-10 19:20:46 +02:00
Julian Krings 8f5f44bc96 open vscode after studio world was created 2025-07-10 18:24:50 +02:00
Julian Krings 6964b99744 fix reading custom mantle flags from json 2025-07-10 16:41:46 +02:00
Julian Krings 11cfd85f6a allow creation of custom mantle flags for custom mantle components 2025-07-10 16:11:02 +02:00
Julian Krings c5416f54fa add script hook for custom noise 2025-07-09 23:40:04 +02:00
Julian Krings 94c5782490 move loading message to debug 2025-07-09 18:49:55 +02:00
Julian Krings cc49b0f540 hopefully fix eta 2025-07-09 17:37:58 +02:00
Julian Krings 1bc6192c8b merge kts engine into this codebase 2025-07-09 15:16:11 +02:00
Julian Krings 4b0766c097 remove bfs 2025-07-09 14:24:24 +02:00
Julian Krings ec5cb2d646 Merge branch 'dev' into feat/kts
# Conflicts:
#	core/build.gradle.kts
#	core/src/main/java/com/volmit/iris/Iris.java
#	core/src/main/java/com/volmit/iris/engine/object/IrisDimension.java
2025-07-09 14:23:53 +02:00
Julian Krings 0edaeeec99 massive cleanup of maven repos and update dependencies 2025-07-08 23:40:10 +02:00
Julian Krings a0543bbbf2 relocate slimjar 2025-07-08 19:59:45 +02:00
Julian Krings 0b1af51227 add option to disable specific mantle components for testing 2025-07-08 19:52:09 +02:00
Julian Krings 03c5998c02 generate a dummy region for standalone focus biomes 2025-07-08 19:08:12 +02:00
Julian Krings 6b193f695a switch to slimjar 2025-07-08 18:46:59 +02:00
Julian Krings 0a30881f87 add jaxen to runtime dependencies 2025-07-08 16:08:30 +02:00
Julian Krings c01a7def5d add iris data setup hook 2025-07-08 15:37:14 +02:00
Julian Krings 50db1d11a7 move script engine libraries into the iris plugin folder 2025-07-08 12:30:21 +02:00
Julian Krings 2e2ea8f1e4 use paper plugin loader to avoid maven central warning 2025-07-08 09:19:57 +02:00
Julian Krings badf108d56 Merge branch 'dev' into feat/kts
# Conflicts:
#	core/build.gradle.kts
#	core/src/main/java/com/volmit/iris/Iris.java
#	core/src/main/java/com/volmit/iris/core/project/SchemaBuilder.java
#	core/src/main/java/com/volmit/iris/util/io/IO.java
#	core/src/main/resources/plugin.yml
2025-07-08 00:36:30 +02:00
Julian Krings f9888d19a5 whoops forgot to update publish task 2025-07-08 00:24:03 +02:00
Julian Krings fbc5cee300 if it still doesn't work, idk what will 2025-07-08 00:22:31 +02:00
Julian Krings 12777bc3f0 does this shit work now?? 2025-07-07 23:59:42 +02:00
Julian Krings 9324b1b5c0 fix api file generation 2025-07-07 23:42:30 +02:00
Julian Krings 2ecb555619 add publishing for the api artifact 2025-07-07 23:37:37 +02:00
Julian Krings 20c7891c2f add api generation task 2025-07-07 22:54:35 +02:00
Julian Krings c35c858eee add scripts for engine setup 2025-07-07 21:06:36 +02:00
Julian Krings cb93e78242 fix safeguard error 2025-07-07 13:18:31 +02:00
Julian Krings c9ed4519a8 remove multiverse core incompatibility warning (#1212) 2025-07-06 20:00:06 +02:00
Julian Krings 86d986dfbc fix deposits spawning in columns 2025-07-06 19:40:14 +02:00
Julian Krings 54402faea8 fix automatic deepslate variant replacement 2025-07-04 13:18:28 +02:00
Julian Krings 77b4253624 fix sentry safeguard info 2025-07-04 12:34:20 +02:00
Julian Krings 44af23ba2e add check for new dimension types 2025-07-04 00:06:55 +02:00
Julian Krings 4e8079e431 use ticket queue for pregen by default 2025-07-04 00:01:16 +02:00
Julian Krings 39f65d02bf 1.21.6/7 Support (#1210) 2025-07-03 23:58:31 +02:00
Julian Krings 8c025a9ba2 clear snippet lists on hotload 2025-07-03 23:45:27 +02:00
Julian Krings 7119fa8ef7 Merge pull request #1207 from VolmitSoftware/feat/byte_buddy
Feat/byte buddy
2025-07-03 23:24:30 +02:00
Julian Krings 6f0b2b6bba initialize generators for isolated focus biomes / regions 2025-06-27 12:21:59 +02:00
Julian Krings 0ae1334a57 fix snippet tab completion 2025-06-27 12:20:25 +02:00
Julian Krings 5d28563b7c implement generate surface for ores 2025-06-24 19:43:46 +02:00
Julian Krings cf8243a000 make caves more deterministic 2025-06-24 19:37:37 +02:00
Julian Krings cca0bed482 fix cave max size calculation 2025-06-24 16:53:26 +02:00
Julian Krings a802edc375 add some options for customizing caves 2025-06-23 17:16:45 +02:00
Julian Krings 3677931114 improve snippet finder 2025-06-23 15:51:53 +02:00
Julian Krings e38dae0a32 fix required properties for json schemas 2025-06-23 15:51:29 +02:00
Julian Krings d537459c5a Merge branch 'dev' into feat/byte_buddy
# Conflicts:
#	build.gradle.kts
#	core/build.gradle.kts
#	core/src/main/java/com/volmit/iris/Iris.java
2025-06-23 14:24:43 +02:00
Julian Krings 67398174bb cache iris worlds in a map for datapack generation 2025-06-23 12:25:39 +02:00
Julian Krings 0a0f77ff58 fix splash being shown twice 2025-06-23 12:25:38 +02:00
Julian Krings e5cb4d82a3 Add KGenerators support (#1209) 2025-06-21 12:05:56 +02:00
Julian Krings 9d44ac0b47 fix caves being non-deterministic 2025-06-21 12:04:39 +02:00
Julian Krings 7f6d65a13e fix worm ignoring the breakSurface option 2025-06-21 12:01:06 +02:00
Julian Krings ad720f4aa2 fix worm using xStyle for y and z 2025-06-21 11:51:49 +02:00
Julian Krings 80548f753c isolate java agent to prevent class loader issues 2025-06-21 11:48:58 +02:00
Julian Krings c9c8a9e412 fix null pointers in pack trim method 2025-06-16 22:53:56 +02:00
Julian Krings d048c073ac disable trim for fallback pack download 2025-06-16 22:53:11 +02:00
Julian Krings 2929a1f0a7 update Script Engine to e08b6f893e 2025-06-16 22:44:15 +02:00
Julian Krings b6f9f68b9f add global preprocessor setting 2025-06-16 22:43:26 +02:00
Julian Krings da2dd42e28 invert suppressed error reporting for dev enviroments 2025-06-16 16:59:24 +02:00
Julian Krings 88360ef772 Merge branch 'dev' into feat/byte_buddy
# Conflicts:
#	build.gradle.kts
#	core/src/main/java/com/volmit/iris/Iris.java
2025-06-15 23:00:39 +02:00
Julian Krings 73787e21d2 Merge branch 'dev' into feat/kts
# Conflicts:
#	build.gradle
#	core/src/main/java/com/volmit/iris/Iris.java
#	core/src/main/java/com/volmit/iris/util/io/IO.java
2025-06-15 22:54:56 +02:00
Aidan Aeternum 840608a40f v+ 2025-06-14 16:49:22 -04:00
Aidan Aeternum fcedc35635 Merge pull request #1205 from VolmitSoftware/dev
3.6.11
2025-06-14 16:47:20 -04:00
Julian Krings 32d9a5e40a make sentry engine context hotload safe 2025-06-14 11:46:51 +02:00
Julian Krings 8df6253604 add safeguard info to sentry reports 2025-06-14 11:44:30 +02:00
Julian Krings 01b62c13b6 fix deleting mantle temp files before they are fully written 2025-06-14 11:18:14 +02:00
Julian Krings f32f73e65a disable error reporting in dev environment 2025-06-13 12:27:03 +02:00
Julian Krings 7b7118fe0d add id for servers hash of jvm name & version, processor info, memory size and plugins 2025-06-13 12:26:56 +02:00
Julian Krings 851ac18f0d implement custom conditions for mythic mobs (#1204) 2025-06-11 13:23:09 +02:00
Julian Krings 37be7ca847 don't send json and zip file closed exceptions to sentry 2025-06-11 13:14:28 +02:00
Julian Krings ed67b4d3c2 fix spawners not having entities due to using old format 2025-06-11 13:03:31 +02:00
Julian Krings b62ac875d5 cleanup gradle 2025-06-10 16:48:00 +02:00
Julian Krings 4c3f95b0e1 Merge branch 'dev' into feat/byte_buddy
# Conflicts:
#	build.gradle
#	core/src/main/java/com/volmit/iris/Iris.java
2025-06-10 13:33:56 +02:00
Julian Krings 8712c8874c disable global pregen cache by default for now 2025-06-09 23:27:12 +02:00
Julian Krings ccc3bab8e0 allow carving recursion 2025-06-09 23:25:33 +02:00
Julian Krings 113f25dab8 more sentry context 2025-06-09 23:17:47 +02:00
Julian Krings cd80acdc7d fix IndexOutOfBoundsException when getting the selection from the wand 2025-06-09 19:50:50 +02:00
Julian Krings 9316ea9e5b fix npe when creating cuboids 2025-06-09 19:49:06 +02:00
Julian Krings e2a3f25dcb fix load in parallel method 2025-06-09 19:42:03 +02:00
Julian Krings 944cc19ebc use proper shuffling algorithm for the loot 2025-06-09 19:35:43 +02:00
Julian Krings 172e234514 fix divide by zero in the engine svc 2025-06-09 19:19:00 +02:00
Julian Krings f9dac8a3a1 fix compile while missing the sentry auth token 2025-06-09 18:40:43 +02:00
Julian Krings eefbbab7ee remove legacy chars before minimessage deserialization 2025-06-09 18:39:11 +02:00
Julian Krings 9cf567e1ff remove unused util to fix class not found exception 2025-06-09 13:47:24 +02:00
Julian Krings b8ee7561dd fix failing to create temp file when user deleted the temp directory 2025-06-09 13:44:57 +02:00
Julian Krings c0d17742e8 fix release task for sentry 2025-06-09 13:43:00 +02:00
Julian Krings a88d389e0f bump gradle wrapper to 8.14.2 and switch to kotlin dsl (#1203) 2025-06-09 12:45:27 +02:00
Julian Krings b341089996 v+ 2025-06-09 02:12:14 +02:00
Julian Krings 9cbfd5a10b Merge pull request #1202 from VolmitSoftware/dev
3.6.10
2025-06-09 02:11:22 +02:00
Julian Krings fdaf8ff9d3 relocate sentry to fix incompatibility with some plugins using a very old sentry version 2025-06-09 00:17:38 +02:00
Julian Krings e63d84c052 remove server name from reports 2025-06-09 00:16:58 +02:00
Julian Krings 0d103a934a fix ticking engine players in the wrong dimension 2025-06-08 12:43:56 +02:00
Julian Krings 8119207254 sentry changes
- info message on enabling
- add release tag
- disable uncaught exception handling to prevent reporting other plugins issues
2025-06-08 12:29:24 +02:00
Julian Krings b66e6d8335 correct sentry dsn 2025-06-07 14:00:22 +02:00
Julian Krings ce2b62f5ae add sentry release task 2025-06-06 18:37:11 +02:00
Julian Krings c767b6c8e8 check sentry env token as fallback 2025-06-06 18:36:41 +02:00
Julian Krings 67328d7d10 remove test exception for sentry 2025-06-06 17:35:13 +02:00
Julian Krings 329e136a66 implement opt-out auto reporting with sentry (#1201) 2025-06-06 17:33:16 +02:00
Julian Krings 52f87befa2 return noop pregen cache when the service is disabled and write pregen cache to temp file first then replace real one 2025-06-06 17:24:23 +02:00
Julian Krings 2ee22db072 fix mantle write failing on windows 2025-06-06 16:49:42 +02:00
Julian Krings cc27e87376 fix world height decreases deleting the whole chunk 2025-06-05 20:34:25 +02:00
Julian Krings abb1d9cd62 add bytebuddy binding for 1.21.5 2025-06-05 20:26:16 +02:00
Julian Krings e0ad029c3d Merge branch 'dev' into feat/byte_buddy
# Conflicts:
#	build.gradle
#	core/src/main/java/com/volmit/iris/engine/platform/BukkitChunkGenerator.java
2025-06-05 19:27:01 +02:00
Julian Krings 5705caa1ba fix benchmarking not disabling properly 2025-06-04 20:20:48 +02:00
Julian Krings a56cd4c268 use block data use for slice 2025-06-04 16:32:29 +02:00
Julian Krings 48cc6bb49c improve mantle writer speeds 2025-06-04 15:03:00 +02:00
Aidan Aeternum 635ee02459 v+ 2025-06-02 18:48:01 -04:00
Aidan Aeternum 2c7b7c8c91 Merge pull request #1199 from VolmitSoftware/dev
3.8.9
2025-06-02 18:47:38 -04:00
Julian Krings ad0662be54 initial 1.21.5 bindings 2025-06-02 18:18:17 +02:00
Julian Krings 835c8422f1 calculate spawn location async 2025-06-02 17:42:36 +02:00
Julian Krings 2c60192de3 use scheduled thread pool instead of loopers for the EngineSVC 2025-06-02 17:29:38 +02:00
Julian Krings adb7188eb9 refactor mantle cleanup 2025-06-02 17:29:30 +02:00
Julian Krings 2436ebb857 Merge pull request #1198 from VolmitSoftware/feat/versioned_mantle 2025-06-02 17:28:58 +02:00
Julian Krings ad85a0bbd1 implement marker exhaustionChance 2025-05-29 23:17:23 +02:00
Julian Krings 22ac9ebf47 update nexo api to release 1.6.0 2025-05-29 14:18:30 +02:00
Julian Krings 3cb9585dd8 fix pregenerator not closing when a new one is started 2025-05-28 21:48:41 +02:00
Julian Krings f4d1177c51 fix cave fluid ignoring fluid palette 2025-05-28 12:57:11 +02:00
Julian Krings b132862a60 update last use for in use tectonic plates 2025-05-27 22:50:30 +02:00
Julian Krings 2452a2f633 cleanup of the mantle trimmer / EngineSVC 2025-05-27 16:23:02 +02:00
Julian Krings f0476fea9b implement version header for tectonic plates 2025-05-26 20:54:12 +02:00
Julian Krings 90c6457d37 write to plates to a temp file first and then move it into the mantle dir 2025-05-26 17:29:15 +02:00
Julian Krings 61301ffd4d close engines using a shutdown hook 2025-05-26 17:29:15 +02:00
Julian Krings e42317139d fix engines not closing on server stop 2025-05-26 17:29:15 +02:00
Julian Krings f68600464b remove redundant method from NMSBinding1X 2025-05-25 18:12:48 +02:00
Julian Krings 97ddfd309b fix updater not working 2025-05-21 13:05:07 +02:00
Aidan Aeternum 5d42c5cae0 v+ 2025-05-20 12:32:04 -04:00
Aidan Aeternum 52fecb48d8 Merge pull request #1196 from VolmitSoftware/dev
3.6.8
2025-05-20 12:31:35 -04:00
Julian Krings ce29dc98c3 fix spawn markers not being removed 2025-05-16 12:41:31 +02:00
Julian Krings 463e3d9658 Merge pull request #1193 from VolmitSoftware/feat/faster_pregen
Feat/faster pregen
2025-05-16 12:29:41 +02:00
Julian Krings 9152b25d51 fix pack hash calculation 2025-05-16 12:22:35 +02:00
Julian Krings bb9c72e414 Partially Merge pull request #1192 from dan28000/Iris 2025-05-16 12:11:46 +02:00
Julian Krings 976648340e add pregen method that doesn't use waiting threads 2025-05-16 12:02:38 +02:00
Julian Krings 5958bcb22e update Script Engine to 41396a77d5 2025-04-28 21:40:44 +02:00
Julian Krings 8eb35aa8be update Script Engine to b5e2cc6e 2025-04-28 21:24:54 +02:00
Julian Krings 49ce84a9e9 Merge branch 'dev' into feat/faster_pregen
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/pregenerator/methods/AsyncPregenMethod.java
2025-04-27 13:56:28 +02:00
Julian Krings 6724b0f4c5 minor cleanup and improved safeguard for missing dimension types 2025-04-25 21:18:33 +02:00
Julian Krings e72abc8c39 generate json schemas for intelij 2025-04-24 16:00:23 +02:00
Julian Krings 0a2f35dd8d update the scripting engine and fix cached file extension 2025-04-23 21:24:14 +02:00
Julian Krings c597c55c2c replace rhino scripting engine with kotlin scripting 2025-04-23 14:21:09 +02:00
Aidan Aeternum f93995e152 v+ 2025-04-21 20:27:43 -04:00
Aidan Aeternum 7a5a2e5909 Merge pull request #1191 from VolmitSoftware/dev
3.6.7
2025-04-21 20:27:18 -04:00
dan28000 e15d31a0ed would be nice to display the right number 2025-04-21 22:59:29 +02:00
dan28000 09cdd61a68 fix method
when having relocated worlds folder this method would fail making warning appear
2025-04-21 20:45:20 +02:00
dan28000 0575cd85c8 delete old stuff
delete legacy command
2025-04-21 20:44:38 +02:00
Julian Krings 2008975a8a expose most of the dimension type options 2025-04-17 17:30:29 +02:00
Julian Krings aad1d3815a Merge branch 'dev' into feat/byte_buddy 2025-04-16 23:18:49 +02:00
Julian Krings 86c64f99e9 filter out non iris pack directories in allPacks stream 2025-04-16 23:01:57 +02:00
Julian Krings 6577f4a5de rough safeguard for missing dimension type 2025-04-16 22:02:42 +02:00
Julian Krings 3415e7c7af implement complete world height isolation 2025-04-16 21:52:30 +02:00
Pixel f1c72974fd Cleanup (#1186)
* Remove server benchmark command

* Moved lazy pregen to dev
2025-04-15 15:53:50 +02:00
Julian Krings 26e2e20840 Feat/pregen cache (#1190)
Add cache to pregen to skip already generated chunks faster
2025-04-14 21:26:39 +02:00
Julian Krings c00dcf205b fix deadlock when closing pregen method while using modified concurrency 2025-04-14 21:25:31 +02:00
Julian Krings a10a784c3b add max concurrency setting for pregen 2025-04-14 21:25:31 +02:00
Julian Krings f99cc61042 add setting to use a virtual thread executor instead of MultiBurst for async pregen 2025-04-14 21:25:31 +02:00
Julian Krings d2a1e5cc1e fix duplicate & redundant purpur recommendations 2025-04-14 21:23:52 +02:00
Julian Krings 3e2c0fa025 Decrease MSPT impact of throwing ender eyes in Iris worlds 2025-04-14 21:23:37 +02:00
Julian Krings 9c151abac7 replace world load context injection with bytecode injections 2025-04-10 16:26:58 +02:00
Aidan Aeternum ab4770400e v+ 2025-04-10 04:49:32 -04:00
Aidan Aeternum 1a810d5d62 Merge pull request #1184 from VolmitSoftware/dev
3.6.6
2025-04-10 04:49:02 -04:00
Julian Krings 536d20bca7 remove context injection on world init 2025-04-09 12:16:03 +02:00
Julian Krings 9a691ac5b4 setup runServer for all supported versions 2025-04-09 10:57:35 +02:00
Julian Krings 71078a20a9 Don't inject world load context for main worlds to prevent incompatibilities 2025-04-08 21:15:19 +02:00
Julian Krings 566fca2b52 Add info message for failed increase of worker threads 2025-04-07 19:00:06 +02:00
Julian Krings 74e2576ca2 Prevent saving the iris level stems 2025-04-06 16:31:35 +02:00
Julian Krings fb0bc112e3 increase worker threads on paper servers during pregen 2025-03-30 14:31:11 +02:00
Julian Krings 407e51378c fix applying x offset to z coords in Spiraler 2025-03-27 15:02:47 +01:00
Julian Krings c468eb1ab1 make pregen use block radius as input 2025-03-27 15:02:47 +01:00
Aidan Aeternum bdb7cc61e5 v+ 2025-03-26 15:41:35 -04:00
Aidan Aeternum e8f9e841c4 Merge pull request #1181 from VolmitSoftware/dev
3.6.5
2025-03-26 15:41:06 -04:00
Julian Krings 1b1b9d97b7 update overworld pack to 31020 2025-03-25 19:15:47 +01:00
Julian Krings 24355064ff add safeguard for missing dimension types to prevent world corruption 2025-03-25 19:14:20 +01:00
Julian Krings 06a45056d9 use flat level source instead of trying to get the levelstems 2025-03-22 12:38:25 +01:00
Aidan Aeternum dfe4894be7 v+ 2025-03-18 16:40:33 -04:00
Aidan Aeternum 8eb2287ec0 Merge pull request #1178 from VolmitSoftware/dev
3.6.4
2025-03-18 16:34:22 -04:00
Julian Krings c4f0722614 improve datapack setup speed 2025-03-18 16:18:37 +01:00
Julian Krings 7fa1484b21 fix levelstem injection not working for main worlds 2025-03-18 16:18:37 +01:00
Julian Krings 1c5eb8b910 automatically update vanilla dimension type if present in Iris datapack 2025-03-18 16:18:37 +01:00
Julian Krings 94bf530d93 add setting to enable changing to the vanilla dimension height values 2025-03-18 16:18:37 +01:00
Julian Krings 686ae57b5b switch from world preset to direct level stems 2025-03-18 16:18:37 +01:00
Julian Krings a911685aaf Fix world gen datapack incompatibilities 2025-03-18 16:18:37 +01:00
Aidan Aeternum 6899761ca9 v+ 2025-03-06 17:06:34 -05:00
Aidan Aeternum a58958fd62 Merge pull request #1176 from VolmitSoftware/dev
3.6.3
2025-03-06 17:04:57 -05:00
Julian Krings 307f3c9158 implement version specific overworld tag (#1175) 2025-03-06 15:57:30 +01:00
Julian Krings 4f275c2e06 Merge pull request #1174 from tavaresjoshua8/master
Fix: Nexo 1.0
2025-03-06 15:55:54 +01:00
tavaresjoshua8 7e4e3f3cd8 Fix: Nexo 1.0 Release 2025-03-05 13:43:26 -07:00
Aidan Aeternum 84e5add564 v+ 2025-02-23 14:39:52 -05:00
Aidan Aeternum 4d4adbb76f Merge pull request #1172 from VolmitSoftware/dev
3.6.2
2025-02-23 14:39:29 -05:00
Julian Krings ff2f285784 isolate iris world height into it's own dimension types (#1171) 2025-02-20 23:41:19 +01:00
Aidan Aeternum 8b1636e78a v+ 2025-02-14 16:46:51 -05:00
Aidan Aeternum 3bdad10562 Merge pull request #1170 from VolmitSoftware/dev
3.6.1
2025-02-14 16:46:22 -05:00
Julian Krings ac03a977aa Mob spawning fixes (#1169)
* fix cooldown being 0 in most cases

* fix max entity count for spawners
2025-02-11 23:06:47 +01:00
Aidan Aeternum d7270f66e1 v+ 2025-02-06 16:16:11 -05:00
Aidan Aeternum b220b1bffa Merge pull request #1168 from VolmitSoftware/dev
3.6.0
2025-02-06 16:15:05 -05:00
Julian Krings 4796fe98cb fix modern tile states not applying properly 2025-02-05 15:20:34 +01:00
Julian Krings ece905ec6e fix registry lookup failing on some server versions 2025-02-04 21:11:08 +01:00
Julian Krings 53c9e7c04c drop 1.19.x support (#1167) 2025-02-04 13:04:48 +01:00
Julian Krings 29f6f52443 add noise based spread type (#1164) 2025-02-04 13:04:28 +01:00
Julian Krings a778cc51a6 Expression functions (#1165) 2025-02-04 13:03:49 +01:00
Julian Krings c6963d0cd3 fix world delete command crashing servers (#1166) 2025-02-04 13:03:33 +01:00
pixel 489844f61b Updated README.md to JDK21 2025-02-03 22:19:38 +01:00
Aidan Aeternum 4d1b0246ca v+ 2025-02-01 10:55:58 -05:00
Aidan Aeternum 13f3511fa8 Merge pull request #1159 from VolmitSoftware/dev
3.5.9
2025-02-01 10:54:00 -05:00
Julian Krings f6f2766315 fix ignoring some custom blocks (#1158) 2025-01-30 13:12:15 +01:00
Aidan Aeternum 56530a1245 v+ 2025-01-29 17:51:36 -05:00
Aidan Aeternum 210a1f29a7 Merge pull request #1156 from VolmitSoftware/dev
3.5.8
2025-01-29 15:51:16 -07:00
Julian Krings 847bf972ae improve performance related to custom blocks (#1155) 2025-01-27 15:22:10 +01:00
Aidan Aeternum e5d21fdf7e v+ 2025-01-25 09:48:00 -07:00
Aidan Aeternum 0b2fd3b358 v+ 2025-01-25 09:47:17 -07:00
Aidan Aeternum 10484d1226 Merge pull request #1153 from VolmitSoftware/dev
3.5.7
2025-01-25 09:46:58 -07:00
Julian Krings ce0092c52a Fix creation deadlock (#1154) 2025-01-25 14:54:21 +01:00
Julian Krings 474e033c2b Feat/1.21.4 (#1152) 2025-01-24 18:03:12 +01:00
Aidan Aeternum 62aad1f497 v+ 2025-01-23 14:46:15 -07:00
Aidan Aeternum 32b5157682 Update build.gradle 2025-01-23 14:44:53 -07:00
Aidan Aeternum 70717ea282 Merge pull request #1151 from VolmitSoftware/dev
3.5.6
2025-01-23 14:44:24 -07:00
Julian Krings 15975f108c Fix compile 2025-01-22 22:52:21 +01:00
Julian Krings 66c66e82c1 remove "smart" vanilla height (#1150) 2025-01-22 22:47:39 +01:00
Julian Krings 4f6da95d8e fix radius calculation skipping pieces (#1149) 2025-01-22 22:47:28 +01:00
Aidan Aeternum b37ccbdf01 v+ 2025-01-17 08:27:58 -05:00
Aidan Aeternum 30dbe0881a Merge pull request #1145 from VolmitSoftware/dev
3.5.5
2025-01-17 05:26:26 -08:00
Julian Krings 20ad4657a9 fix null pointer preventing the entity schema from generating (#1142) 2025-01-15 11:30:55 +01:00
Julian Krings d4986f42a6 Fix decorator determinism (#1144) 2025-01-15 11:30:34 +01:00
Aidan Aeternum 8df15c0c2d v+ 2025-01-12 14:12:43 -08:00
Aidan Aeternum 24e1c578c8 Merge pull request #1139 from VolmitSoftware/dev
3.5.4
2025-01-12 14:10:50 -08:00
Julian Krings 1c3bff7559 fix sculk veins not properly placing with decorators (#1137) 2025-01-12 15:37:10 +01:00
Julian Krings a09657b4d0 precalculate pack hash on engine setup (#1138) 2025-01-12 15:36:54 +01:00
Aidan Aeternum 910220d3ca v+ 2025-01-08 18:22:26 -08:00
Aidan Aeternum fc05c24e3a Merge pull request #1135 from VolmitSoftware/dev
3.5.3
2025-01-08 18:20:43 -08:00
Julian Krings e1a7e772cf fix floating objects (#1134) 2025-01-07 22:03:53 +01:00
Aidan Aeternum 3c6411c322 v+ 2025-01-07 11:22:52 -08:00
Aidan Aeternum 628761affa Merge pull request #1130 from VolmitSoftware/dev
3.5.2
2025-01-07 11:21:06 -08:00
Julian Krings fa7b0f68ff Merge pull request #1129 from VolmitSoftware/feat/deposits
Feat/deposits
2025-01-05 16:25:03 +01:00
Aidan Aeternum 487d0ac237 Merge pull request #1128 from VolmitSoftware/dev
3.5.1
2025-01-04 09:57:30 -08:00
Julian Krings e79e3fbe45 implement <1 deposit spawn chance 2025-01-04 03:46:08 +01:00
Julian Krings 23a0ab23aa fixes to deposit clump generation and placement 2025-01-04 03:41:55 +01:00
Julian Krings c78ffab948 fix deposit clump calculation 2025-01-03 00:33:49 +01:00
Julian Krings d58f497b71 v+ 2025-01-02 23:21:32 +01:00
Julian Krings 3284ab84c5 fix biome&region deposits being placed ^2 2025-01-02 23:15:27 +01:00
Julian Krings 5cf0ac9315 Merge pull request #1115 from VolmitSoftware/v3.4.3
V3.5.0
2025-01-02 19:37:22 +01:00
Julian Krings 4e4e820826 update overworld tag and make first setup use the tag rather than current codebase 2025-01-02 14:38:45 +01:00
Julian Krings cb6e4570f4 add fallback loot mode 2025-01-02 13:28:20 +01:00
Julian Krings fb59c7370d partially revert commit c93cb19563 2025-01-02 12:33:59 +01:00
Julian Krings c16e65f0a8 add force place option to IrisJigsawStructure 2025-01-02 12:33:59 +01:00
Julian Krings d9681edf62 relocate IrisLootEvent to the proper package 2025-01-02 12:33:59 +01:00
repix 520c156d5f Repix gradle update 2024-12-29 16:47:09 +01:00
Julian Krings 5c26ec2521 fix vanilla loottables not applying 2024-12-28 00:43:01 +01:00
Julian Krings d192e2b6d1 add tabcomplete for vanilla loottables and fix some multi version compat 2024-12-27 18:14:29 +01:00
Julian Krings 5b60b655ed some more optimizations 2024-12-26 17:54:02 +01:00
Julian Krings 97c7ac0528 some more bstats metrics 2024-12-26 11:22:06 +01:00
Julian Krings 090ff730a7 remove item type caching for schemas 2024-12-24 10:09:22 +01:00
Julian Krings c1f797e7c9 update bstats 2024-12-23 23:37:01 +01:00
Julian Krings 4a1a6b80a6 minor performance increase 2024-12-23 23:36:25 +01:00
Julian Krings e189b2389c add vanilla loottables 2024-12-22 16:39:41 +01:00
Julian Krings 3265447536 woops 2024-12-14 16:43:42 +01:00
Julian Krings 8c7c9f89e1 add missing nullability annotations to data providers 2024-12-14 16:42:01 +01:00
Julian Krings d7a991b9b3 fix NexoDataProvider 2024-12-14 16:40:04 +01:00
Julian Krings abf6c93f2e allow custom data provider registration 2024-12-14 16:39:41 +01:00
Julian Krings 1b76a66760 update iris core to jdk 21 2024-12-14 16:23:29 +01:00
RePixelatedMC c83ac67b47 New features 2024-12-05 10:28:04 +01:00
Julian Krings 1dca502a90 add more safety to mantle 2024-11-27 19:45:08 +01:00
Julian Krings cacef8c8fc add legacy tile data reader 2024-11-25 20:05:21 +01:00
Julian Krings 623fd45ef4 fixes to nexo support 2024-11-24 19:00:51 +01:00
Julian Krings 007b4b0b53 whoops 2024-11-23 21:09:54 +01:00
Julian Krings b6bacee095 replace Oraxen with Nexo support 2024-11-23 21:03:14 +01:00
Julian Krings d5251350a1 initial 1.21.3 support 2024-11-16 20:19:28 +01:00
Julian Krings 1f9c72d093 fix compile 2024-11-09 13:58:10 +01:00
Julian Krings a1495a10e5 updater fixes 2024-11-08 21:01:58 +01:00
Julian Krings 11ae48bea1 revert previous 2024-10-31 21:33:24 +01:00
Julian Krings b0f4b29a2d ehh 2024-10-31 21:05:07 +01:00
Julian Krings c38bb1cd01 stability improvements for the chunk updater 2024-10-30 15:59:31 +01:00
Julian Krings 7faa727bd2 midsave ChunkUpdater improvements 2024-10-29 22:12:22 +01:00
Julian Krings 9e40259ca2 fix LegacyTileData not placing 2024-10-17 19:18:11 +02:00
Julian Krings 94a7692735 add force place for stronghold 2024-10-17 18:48:17 +02:00
Julian Krings 8b803a87f0 rename CrucibleDataProvider to MythicCrucibleDataProvider to fit naming conventions 2024-10-17 11:28:50 +02:00
Julian Krings 3ae6e92eaf woops 2024-10-16 22:05:47 +02:00
Julian Krings 0f3c52a5aa cleanup CrucibleDataProvider and fix furniture support 2024-10-16 21:59:38 +02:00
Schroddinger 747be7aa09 MythicCrucible support. Currently under testing.
(cherry picked from commit 06d0f0748bfb3a69b663c2c3604b65f52cb4c5c2)
2024-10-16 21:37:54 +02:00
Brian Fopiano d651f204f8 Merge pull request #1117 from hUwUtao/master
make javadoc buildable...
2024-10-02 00:43:56 -04:00
stdpi 2bd3ac7a9b Merge branch 'VolmitSoftware:master' into master 2024-09-25 02:00:31 +07:00
Brian Neumann-Fopiano 5e437b34e3 Revert "Update LICENSE.md"
This reverts commit 6c6c9654c1.
2024-09-24 14:58:10 -04:00
stdpi a5ef89a128 add irisDev task to copy javadocs and sources jar 2024-09-22 17:35:00 +07:00
stdpi 558b8e4eca add more jar package (javadocJar sourcesJar) 2024-09-22 16:09:48 +07:00
stdpi 0a001b8a63 fix: javadoc tolerate bad html
and match encoding, ugh
2024-09-22 04:48:18 +07:00
Brian Fopiano 6c6c9654c1 Update LICENSE.md 2024-09-09 16:54:57 -04:00
RePixelatedMC e21fdf46e0 - New Noise color < need further testing >
- Iris schem converter
2024-09-05 21:57:40 +02:00
RePixelatedMC a2ff5f58ed Merge remote-tracking branch 'origin/v3.4.3' into v3.4.3 2024-09-04 15:07:42 +02:00
RePixelatedMC b0eedee519 - Progress bar for /iris std dist
- Noted out that its in chunks for radius in /iris std dist
2024-09-04 15:07:30 +02:00
Julian Krings e101155a4c fix datapack particle rarity 2024-09-01 18:51:23 +02:00
Julian Krings 3c9bcc9bb0 add chunked cuboid iterator 2024-09-01 15:24:49 +02:00
RePixelatedMC 6763844030 back to debug 2024-08-31 18:29:43 +02:00
RePixelatedMC 08fa436885 better 2024-08-31 17:45:09 +02:00
RePixelatedMC df8fa79209 Merge remote-tracking branch 'origin/v3.4.3' into v3.4.3 2024-08-31 17:17:44 +02:00
RePixelatedMC 8041db4f40 - New draw system
- Crazy performance compared to the old system
2024-08-31 17:17:20 +02:00
Julian Krings 18e57e4097 add progressbar to object writing 2024-08-31 16:28:52 +02:00
Julian Krings cc584ba377 add progressbar to object saving 2024-08-31 16:01:25 +02:00
Julian Krings 0c92c20c65 ehm 2024-08-31 14:13:50 +02:00
Julian Krings ec8af56f0d woops 2024-08-28 20:30:43 +02:00
Julian Krings 79341bf562 update adventure api 2024-08-28 14:17:54 +02:00
Julian Krings 3f24d5c8e1 fix seed for structure placement 2024-08-27 15:55:02 +02:00
Julian Krings 66a1739666 v+ 2024-08-25 22:16:40 +02:00
Julian Krings 29007fdbfa fix walls not being rotated 2024-08-25 21:48:10 +02:00
Julian Krings be3e8ebd51 fix MMOItems support 2024-08-25 19:41:35 +02:00
Julian Krings 38ad345f85 potential fix for pregen deadlock 2024-08-25 19:35:49 +02:00
Julian Krings 414f46a08d Merge remote-tracking branch 'origin/master' into v3.4.3 2024-08-21 13:09:11 +02:00
repixelatedmc 9144606688 Biggest 1 line fix ever 2024-08-19 19:36:35 +02:00
Julian Krings effb0f5b91 fix biome colors 2024-08-18 17:02:34 +02:00
Julian Krings bb7bbb6379 fix legacy tile data loading from mantle 2024-08-17 19:01:27 +02:00
Julian Krings dafca8e9db woops 2024-08-17 12:29:08 +02:00
Julian Krings 41b7aec084 Merge branch 'fix_maps' into v3.4.3
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/nms/v1X/NMSBinding1X.java
#	nms/v1_19_R1/src/main/java/com/volmit/iris/core/nms/v1_19_R1/NMSBinding.java
#	nms/v1_19_R2/src/main/java/com/volmit/iris/core/nms/v1_19_R2/NMSBinding.java
#	nms/v1_19_R3/src/main/java/com/volmit/iris/core/nms/v1_19_R3/NMSBinding.java
#	nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/NMSBinding.java
#	nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/NMSBinding.java
#	nms/v1_20_R4/src/main/java/com/volmit/iris/core/nms/v1_20_R4/NMSBinding.java
2024-08-17 12:25:23 +02:00
Julian Krings dfe1cce6de restructure LegacyTileData 2024-08-17 11:59:28 +02:00
Julian Krings fc793592f7 fix old object loading 2024-08-17 00:45:58 +02:00
Brian Neumann-Fopiano c3ac41f894 V+
lol
2024-08-15 12:58:01 -04:00
Julian Krings d0688782b1 fix getEngineData NullPointer 2024-08-15 18:47:27 +02:00
Julian Krings 25b41fe62c add 1.21.1 support 2024-08-15 18:42:23 +02:00
Julian Krings b468478fcb separate tile data into its own option in IrisBlockData 2024-08-15 18:00:18 +02:00
Julian Krings b6dc934198 fix getEngineData NullPointer 2024-08-15 17:12:56 +02:00
Julian Krings f58078e8a0 implement dynamic tile entities 2024-08-14 20:02:08 +02:00
Julian Krings b6457e47e6 fix async tile state 2024-08-13 21:35:17 +02:00
Julian Krings d275466e1e reformat BukkitChunkGenerator::onWorldInit 2024-08-13 20:25:54 +02:00
Julian Krings f9cb107728 catch getEngineData NullPointer 2024-08-13 20:25:34 +02:00
repixelatedmc 68ad206252 Eh yeah should do 2024-08-12 08:08:23 +02:00
Julian Krings 288bead792 fix stronghold locate using wrong seed 2024-08-11 12:03:47 +02:00
Julian Krings de670ddfd5 fix explorer/treasure map 2024-08-11 12:03:23 +02:00
Brian Neumann-Fopiano 79d6f34879 v+ 2024-08-03 14:42:07 -04:00
Julian Krings ceb6c15c97 tall_seagrass isnt sticking out of water anymore
by RePixelatedMC
2024-08-03 18:45:44 +02:00
Julian Krings 7cf43ad7ab fix world creation 2024-08-03 18:04:37 +02:00
Julian Krings ab3397a373 fix pack download command 2024-08-03 15:30:56 +02:00
Julian Krings 8e9f78e982 fix region zoom 2024-08-03 13:03:50 +02:00
Julian Krings 61bbfee640 Update README.md 2024-08-02 18:20:25 +02:00
Brian Neumann-Fopiano cf4796bd12 V+ 2024-07-31 18:45:13 -04:00
Julian Krings bd89d8a308 change version to show 1.21 compat 2024-08-01 00:20:30 +02:00
Julian Krings df2186d70f Merge pull request #1110 from VolmitSoftware/v1_21
1.21 Support
2024-07-31 23:52:10 +02:00
Julian Krings f9638e830f update NMSTools 2024-07-30 12:56:27 +02:00
Julian Krings cd55a7fed4 fix getBiomeColor for 1.19.2 2024-07-27 15:43:09 +02:00
Julian Krings fc890a5ba1 fix missing getBiomeColor method 2024-07-27 15:42:51 +02:00
Julian Krings 2ea54b7f2f 1.21 nms bindings 2024-07-24 19:59:01 +02:00
Brian Fopiano e90e3901f5 Merge pull request #1109 from VolmitSoftware/fixes
Fixes
2024-07-23 15:43:33 -04:00
Brian Fopiano 36d58f29ab Merge pull request #1108 from VolmitSoftware/better_nms
Better NMS
2024-07-23 15:43:21 -04:00
Brian Fopiano 864c7ae27c Merge pull request #1106 from VolmitDev/custom_data
improve custom block/item api to allow custom settings
2024-07-23 15:43:04 -04:00
Julian Krings 6d104a2d1c fix y-coordinate for getting biome 2024-07-23 20:05:05 +02:00
Julian Krings 89e754245d add maxTries to IrisLootTable to prevent crash 2024-07-21 16:00:02 +02:00
Julian Krings 4c68d99c6f fix returning black color in biomes without a color override 2024-07-21 15:50:45 +02:00
Julian Krings f8f0a65f0a add debug info to the updater 2024-07-21 15:25:31 +02:00
Julian Krings 6ff74639b5 use custom nms gradle plugin 2024-07-18 17:17:05 +02:00
Julian Krings af80f882da add slope condition to sear/shore/surface decorators 2024-07-09 16:25:07 +02:00
Julian Krings 84ce7372c3 implement dimension zoom options 2024-07-09 16:23:32 +02:00
Julian Krings e31d39c5f7 fix worldedit selection not updating 2024-07-09 16:19:56 +02:00
Julian Krings 2296c1368b fix WorldEditLink 2024-07-09 16:19:22 +02:00
Brian Fopiano 21d7d96dbc Merge pull request #1107 from RePixelatedMC/master 2024-07-05 00:46:54 -04:00
RePixelatedMC 956e511fe0 Merge remote-tracking branch 'origin/master' 2024-07-04 22:02:42 +02:00
RePixelatedMC e427887f8c Scary 2024-07-04 22:00:50 +02:00
Julian Krings 88ea43fbbe fix post processing for custom blocks not applying to objects 2024-06-28 18:14:52 +02:00
Julian Krings 29526a80a9 improve custom block/item api to allow custom settings 2024-06-25 13:35:34 +02:00
Julian Krings 48f901fc8c handle null pointer 2024-06-25 13:33:44 +02:00
Julian Krings 6c3f3dc889 update oraxen api 2024-06-24 13:17:11 +02:00
Julian Krings 743410d3ee add nms method to get biome color 2024-06-24 13:16:56 +02:00
Brian Neumann-Fopiano acccbb028f V+ 2024-06-18 21:32:35 -04:00
Julian Krings 8b31fdc780 Merge pull request #1105 from VolmitDev/jigsaw_dist
Fix jigsaw spawn distances
2024-06-17 17:07:47 +02:00
CrazyDev22 55017b9ac2 Revert "Merge branch 'mca' into jigsaw_dist"
This reverts commit 0288d35c85, reversing
changes made to 8b3577dd0b.
2024-06-16 14:25:47 +02:00
Julian Krings 0288d35c85 Merge branch 'mca' into jigsaw_dist 2024-06-16 14:23:49 +02:00
Julian Krings 8b3577dd0b make divisor used for generating missing jigsaw placement values changeable 2024-06-16 14:06:24 +02:00
Brian Neumann-Fopiano ced77ec70d V+ 2024-06-09 17:26:08 -04:00
Julian Krings e1e90f5c8a add toolchain resolver 2024-06-08 08:27:17 +02:00
RePixelatedMC dc3487583d Merge pull request #25 from VolmitSoftware/merge
merge
2024-06-05 09:53:26 +02:00
RePixelatedMC e1d73ebcd5 Merge branch 'mca' into merge 2024-06-05 09:53:13 +02:00
RePixelatedMC d7cf3b9500 no gui for now 2024-06-03 10:03:14 +02:00
Brian Fopiano 8ce4e3c6b2 Merge pull request #1104 from RePixelatedMC/master
small changes
2024-06-01 10:30:21 -04:00
RePixelatedMC 76f7d46b22 e 2024-06-01 16:26:21 +02:00
RePixelatedMC cfc52ed909 Tuning + Fixed server distro reading for 1.20.6+ 2024-06-01 16:21:57 +02:00
RePixelatedMC cee94d40e1 Merge branch 'master' of https://github.com/VolmitSoftware/Iris 2024-05-29 10:18:04 +02:00
RePixelatedMC cad7166cbe Merge pull request #24 from VolmitDev/fix_jigsaw
minor changes to jigsaw
2024-05-25 15:41:05 +02:00
Julian Krings c05fdbd0ec Merge pull request #1103 from VolmitDev/fix_jigsaw
minor changes to jigsaw placement
2024-05-25 15:39:07 +02:00
CrazyDev22 c9b26ebaff add better backwards compat 2024-05-25 15:34:51 +02:00
CrazyDev22 bee1973390 fix salt not being a long 2024-05-25 15:34:25 +02:00
RePixelatedMC 57efcab0b5 tall_seagrass isnt sticking out of water anymore 2024-05-22 16:55:07 +02:00
RePixelatedMC b1ab2b84f8 Fixed /iris region going of a chunk instead of x,z coords
Added fast edit commands
- /iris std edit biome is now supported and opens the vscode biome of where you are standing at same with region.
2024-05-22 15:34:45 +02:00
RePixelatedMC 02601e5ee4 Cleanup 2024-05-22 13:32:29 +02:00
RePixelatedMC 070479acf3 Merge branch 'mca' of https://github.com/RePixelatedMC/Iris into PixelatedDev 2024-05-22 13:29:16 +02:00
RePixelatedMC 6b2ba74237 Iris lets the user ingame know when unstable. 2024-05-22 13:28:56 +02:00
RePixelatedMC 619de29710 Merge pull request #23 from VolmitDev/mca
improve iris world protection
2024-05-21 16:39:21 +02:00
CrazyDev22 12556fa98f improve iris world protection 2024-05-21 16:38:28 +02:00
RePixelatedMC bef4c0497f Merge branch 'mca' of https://github.com/RePixelatedMC/Iris into PixelatedDev 2024-05-21 11:57:03 +02:00
RePixelatedMC 9193497d36 Merge pull request #22 from RePixelatedMC/v3.3
V3.3 x MCA?
2024-05-21 11:56:14 +02:00
RePixelatedMC ca6affbde9 Merge branch 'master' into v3.3 2024-05-21 11:48:39 +02:00
RePixelatedMC 81c40ce228 Merge branch 'master' of https://github.com/RePixelatedMC/Iris into PixelatedDev 2024-05-21 11:23:52 +02:00
RePixelatedMC 6ba1d571f2 Fallback type when mm not on server 2024-05-21 11:23:24 +02:00
RePixelatedMC b0663f9b6c Merge branch 'master' into PixelatedDev 2024-05-21 11:22:39 +02:00
RePixelatedMC 3ced6f3e9e Entity Fallback 2024-05-20 14:46:11 +02:00
RePixelatedMC 3146d61efe Merge pull request #19 from VolmitDev/mca
Sync
2024-05-20 14:04:32 +02:00
CrazyDev22 a967b6af85 replace datapack system with a dynamic one 2024-05-20 06:41:38 +02:00
Brian Fopiano db3bc74364 Merge pull request #1092 from VolmitDev/fix_jigsaw
Jigsaw fixes
2024-05-19 18:21:11 -04:00
CrazyDev22 eb19d9a846 Merge branch 'refs/heads/master' into mca
# Conflicts:
#	build.gradle
#	core/src/main/java/com/volmit/iris/core/ServerConfigurator.java
#	core/src/main/java/com/volmit/iris/core/nms/INMSBinding.java
#	core/src/main/java/com/volmit/iris/engine/object/IrisDimension.java
2024-05-19 20:48:13 +02:00
Julian Krings 96dd299de7 Merge pull request #1102 from VolmitDev/v1_20_R4
move datapack upgrade command under dev
2024-05-19 19:11:43 +02:00
CrazyDev22 aee0806aa8 move datapack upgrade command under dev 2024-05-19 19:10:53 +02:00
Brian Fopiano e39f13bbb4 Merge pull request #1101 from VolmitDev/v1_20_R4
add datapack upgrade command
2024-05-19 08:49:15 -04:00
CrazyDev22 52ec80d384 add datapack upgrade command 2024-05-19 14:40:19 +02:00
CrazyDev22 d9f8909bdc fix crash when using iris as main world 2024-05-18 23:36:03 +02:00
CrazyDev22 65aa9dc343 minor headless performance improvements 2024-05-18 23:34:53 +02:00
CrazyDev22 9bf7fdf174 headless performance improvements 2024-05-17 20:05:37 +02:00
CrazyDev22 dafb59e5a8 improve chunk existing check 2024-05-17 16:45:59 +02:00
CrazyDev22 79f86ca87d woops wrong height 2024-05-17 16:39:57 +02:00
RePixelatedMC 1749fcf6b5 Merge pull request #18 from VolmitDev/mca
1.20.4 Headless
2024-05-17 16:04:43 +02:00
CrazyDev22 e1a0481cdf implement Headless for 1.20.4 2024-05-17 16:01:21 +02:00
RePixelatedMC 4576b95814 Fixed width offset with /iris std map when dragging 2024-05-16 17:32:32 +02:00
RePixelatedMC e113fa6a61 Merge branch 'mca' of https://github.com/RePixelatedMC/Iris into PixelatedDev 2024-05-16 16:55:51 +02:00
RePixelatedMC 07228afbff Noise benchmark reworkings 2024-05-16 16:55:14 +02:00
RePixelatedMC afdffc2453 Merge pull request #17 from VolmitDev/fix_jigsaw
Sync jigsaw
2024-05-16 13:41:35 +02:00
CrazyDev22 6348442962 add y lock to fix offset when placing objects 2024-05-16 13:17:17 +02:00
CrazyDev22 fd10c005b0 disable warp to prevent x/z offsets 2024-05-16 13:15:34 +02:00
CrazyDev22 44500d6af9 fix translation not rotating 2024-05-16 13:12:49 +02:00
CrazyDev22 6ddb0b5304 update gson library 2024-05-16 13:12:11 +02:00
Brian Fopiano 8ce0976f88 Merge pull request #1100 from VolmitDev/v1_20_R4
add 1.20.6 support
2024-05-15 19:52:08 -04:00
CrazyDev22 8345a58f2b change safeguard message 2024-05-15 18:11:05 +02:00
CrazyDev22 f92234297e add 1.20.6 support 2024-05-15 18:07:12 +02:00
RePixelatedMC 6a5bcd5990 Merge pull request #16 from VolmitDev/fix_jigsaw
sync Jigsaw
2024-05-15 17:40:29 +02:00
RePixelatedMC 1fe6d9a636 Merge branch 'mca' of https://github.com/RePixelatedMC/Iris into PixelatedDev
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/tools/IrisWorldDump.java
2024-05-15 09:29:48 +02:00
RePixelatedMC 97f6ab66c3 sync 2024-05-15 09:13:00 +02:00
RePixelatedMC b78c182d94 Map for studio 2024-05-13 16:30:18 +02:00
CrazyDev22 09a0f83013 add terminating pool overwrite 2024-05-11 12:51:29 +02:00
CrazyDev22 f68d7420e3 woops forgot description for SpreadType 2024-05-11 12:50:40 +02:00
CrazyDev22 55c0fa5f32 add command to sample the distances between structures 2024-05-10 15:58:57 +02:00
CrazyDev22 8b5d1d0298 rewrite jigsaw placement logic to prevent placements from being too close 2024-05-10 15:58:26 +02:00
CrazyDev22 b70e94dc65 make manual jigsaw place use the same calculations as normal 2024-05-10 15:43:01 +02:00
CrazyDev22 49a6552168 add forcePlace option to suppress placement checks 2024-05-10 15:41:42 +02:00
Brian Fopiano 0dd6892b28 Merge pull request #1097 from RePixelatedMC/master
Eco Items support
2024-05-09 15:44:13 -04:00
RePixelatedMC fec587edad Update README.md 2024-05-07 22:42:28 +02:00
RePixelatedMC 712937d661 Merge pull request #15 from VolmitDev/ecoitems
add Ecoitems support
2024-05-04 16:03:08 +02:00
CrazyDev22 ace434e7b0 add EcoItems support 2024-05-04 13:21:55 +02:00
RePixelatedMC 1e5aa6e8b0 Merge branch 'mca' of https://github.com/RePixelatedMC/Iris into PixelatedDev 2024-05-04 11:26:47 +02:00
RePixelatedMC 441b27f8f4 Merge branch 'VolmitSoftware:master' into mca 2024-05-04 11:26:23 +02:00
RePixelatedMC 3f3bf70212 Merge pull request #14 from VolmitDev/mca
Per World Dimension Types + better Datapack reload
2024-05-04 11:26:05 +02:00
RePixelatedMC a12eddbedf sync 2024-05-04 11:25:17 +02:00
CrazyDev22 12abc1709e add missing method to NMSBinding1X 2024-05-03 21:18:00 +02:00
CrazyDev22 9e6035e7b4 remove debug code 2024-05-03 21:17:02 +02:00
CrazyDev22 20b41d65d3 fixes to the per world dimensions + optimizations with the register method 2024-05-03 21:01:10 +02:00
CrazyDev22 1593bb2088 make smartVanillaHeight also adjust the logicalHeight 2024-05-03 19:57:34 +02:00
CrazyDev22 d4f9a20379 more removal of unnecessary settings 2024-05-03 19:56:43 +02:00
CrazyDev22 6e247597a4 fix loading the wrong IrisDimension on startup 2024-05-03 19:55:58 +02:00
CrazyDev22 8a8be4545c remove unnecessary minecraft dimension height calculations 2024-05-03 19:11:55 +02:00
CrazyDev22 05f4955989 implement proper datapack reload+per world height 2024-05-03 19:01:33 +02:00
CrazyDev22 93469fb3b4 include translate option into the bounding box of jigsaw pieces 2024-05-01 20:01:10 +02:00
RePixelatedMC a70258d69f Merge branch 'fix_jigsaw' of https://github.com/VolmitDev/Iris into PixelatedDev 2024-05-01 18:55:02 +02:00
CrazyDev22 c653d852e4 make jigsaw place command use the same method as normal gen 2024-05-01 18:46:22 +02:00
CrazyDev22 40163d25b8 add object shink command for fixing too large/small bounding boxes for objects 2024-05-01 18:44:45 +02:00
CrazyDev22 5b4265783e fix object w/h/d not changing with rotation 2024-05-01 18:42:23 +02:00
CrazyDev22 720417b6c8 fix shrinkwrap method 2024-05-01 18:39:42 +02:00
RePixelatedMC bb78f412e0 Sync / Progress on dump 2024-05-01 12:21:48 +02:00
RePixelatedMC a919b91efb Whohoo! we can now read mca files! 2024-04-30 11:14:35 +02:00
Brian Neumann-Fopiano a3dcf031c9 v+ , and the other v+ lol 2024-04-29 15:45:41 -04:00
RePixelatedMC 56eb4b6b84 Iris is small now! still a bit too big for spigot probably 2024-04-28 11:56:18 +02:00
RePixelatedMC 393cb362db - "Hey stop that! Its my World not yours."
- Safeguard displays itself before the worlds now.
- TODO: Minimize jar 7.5 MB > Under Spigots limit
2024-04-28 11:22:10 +02:00
RePixelatedMC 64e422036c Merge branch 'fix_jigsaw' of https://github.com/VolmitDev/Iris into PixelatedDev 2024-04-27 17:14:16 +02:00
RePixelatedMC 468e7ef018 Merge pull request #1094 from RePixelatedMC/master
Update README.md
2024-04-27 12:42:50 +02:00
RePixelatedMC e3e4ecbc5c Update README.md 2024-04-27 12:41:29 +02:00
RePixelatedMC 545ffc0e9d splash cleanup 2024-04-27 12:36:41 +02:00
RePixelatedMC 8125c8d5f4 fixes 2024-04-27 12:18:28 +02:00
RePixelatedMC 304b01d0cf Merge pull request #13 from VolmitDev/mca
add hotload
2024-04-27 10:54:38 +02:00
CrazyDev22 a5f687fd76 fix ServerConfigurator 2024-04-26 18:14:40 +02:00
CrazyDev22 fc1761a55b add nms method to hotload Datapacks 2024-04-26 18:14:22 +02:00
RePixelatedMC e7c9cad7f6 Merge pull request #1093 from RePixelatedMC/3.2.5
3.2.5
2024-04-26 18:06:37 +02:00
RePixelatedMC 979af82122 here crazy 2024-04-26 11:16:09 +02:00
RePixelatedMC 71a62b9c73 Merge pull request #12 from VolmitDev/mca
fix NMSBindings
2024-04-24 17:58:44 +02:00
CrazyDev22 bb020cab25 fix NMSBindings 2024-04-24 17:55:40 +02:00
RePixelatedMC 1ad35c1310 sync multiple mca fixes 2024-04-24 16:01:11 +02:00
CrazyDev22 6470b2f4a9 fix another null pointer 2024-04-23 13:49:30 +02:00
CrazyDev22 c1d5ba55cd fix null pointer 2024-04-23 13:49:10 +02:00
CrazyDev22 100e450514 fix jigsaw piece collision 2024-04-23 13:48:47 +02:00
RePixelatedMC 0f1d1d9860 Merge remote-tracking branch 'VolmitDev/fix_jigsaw' into PixelatedDev 2024-04-23 12:31:26 +02:00
RePixelatedMC 178a462a4e e 2024-04-23 12:29:19 +02:00
RePixelatedMC 8f4ae613f0 Merge branch '3.2.5' of https://github.com/RePixelatedMC/Iris into PixelatedDev
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/commands/CommandIris.java
2024-04-23 11:29:18 +02:00
RePixelatedMC 9e6963b6ce A lot of improvements thanks to CrazyDev! 2024-04-23 11:24:53 +02:00
CrazyDev22 cb9a73c60e rename IrisJigsawDistance to IrisJigsawMinDistance to avoid confusion 2024-04-23 10:28:04 +02:00
CrazyDev22 0e666a4c35 change rng noise for jigsaw component 2024-04-22 14:23:45 +02:00
CrazyDev22 c80138a354 Merge branch 'refs/heads/master' into fix_jigsaw 2024-04-22 14:14:48 +02:00
CrazyDev22 79a4ebcf65 woops 2024-04-19 15:29:28 +02:00
CrazyDev22 a7118aa785 fix jigsaw place command 2024-04-19 15:18:11 +02:00
RePixelatedMC 2555cd23a0 Merge branch 'VolmitSoftware:master' into 3.2.6 2024-04-19 15:17:21 +02:00
RePixelatedMC 484fbeca7b Merge branch '3.2.6' of https://github.com/RePixelatedMC/Iris into PixelatedDev
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/commands/CommandIris.java
#	core/src/main/java/com/volmit/iris/core/safeguard/ServerBootSFG.java
2024-04-19 15:16:46 +02:00
RePixelatedMC 9a45e0df10 /iris support report ( gives server info )
and cleaned the main splash screen and fixed /iris import saying its loaded successfully even if it didnt
2024-04-19 15:15:08 +02:00
RePixelatedMC 9175296fc6 rev2.0 2024-04-19 12:43:57 +02:00
RePixelatedMC 5a4a86aeba - Iris Updater
Updates chunks thats it/
2024-04-19 10:14:16 +02:00
Brian Neumann-Fopiano 768fa7beb5 v+ 2024-04-19 02:47:02 -04:00
Julian Krings 6645eb9806 Merge branch 'VolmitSoftware:master' into fix_jigsaw 2024-04-18 16:04:30 +02:00
CrazyDev22 d7a283c99f fix underwater jigsaw placement 2024-04-18 16:00:47 +02:00
Brian Fopiano 8f019cd794 Merge pull request #1088 from RePixelatedMC/PixelatedDev
3.2.5 iris
2024-04-18 07:13:52 -04:00
CrazyDev22 c1d9cc62cb woops 2024-04-17 21:21:18 +02:00
Julian Krings 010a1e9e91 Merge branch 'VolmitSoftware:master' into fix_jigsaw 2024-04-17 21:16:08 +02:00
CrazyDev22 856c926cde add min distance to jigsaw structures 2024-04-17 21:15:15 +02:00
RePixelatedMC 72ed312654 wops 2024-04-16 18:09:03 +02:00
RePixelatedMC c7fe4723f7 Merge remote-tracking branch 'origin/master' into PixelatedDev 2024-04-16 17:18:51 +02:00
RePixelatedMC bfbea83a4a - Fixed NMS biome height.
- Improved /iris load
- Improved Vanilla height converter
- Removed WorldCreation unstable message
2024-04-16 17:18:02 +02:00
RePixelatedMC 55c58fe896 Merge pull request #1087 from RePixelatedMC/PixelatedDev
Should work
2024-04-16 16:34:12 +02:00
RePixelatedMC 72949e0950 Update IrisSettings.java 2024-04-16 16:32:52 +02:00
CrazyDev22 61c0ddb15b fix random jigsaw rotation and offset 2024-04-16 13:29:06 +02:00
Brian Neumann-Fopiano 9ac4024e4e v+ 2024-04-15 11:43:09 -04:00
RePixelatedMC 7fb2a51a33 sync 2024-04-12 17:08:13 +02:00
RePixelatedMC 5b7c0b2bc3 Merge branch 'PixelatedDev' of https://github.com/RePixelatedMC/Iris into PixelatedDev
# Conflicts:
#	core/src/main/java/com/volmit/iris/engine/object/IrisBiome.java
#	core/src/main/java/com/volmit/iris/engine/object/IrisBiomeGeneratorLink.java
2024-04-09 20:07:50 +02:00
RePixelatedMC fc15175ff9 Improved? 2024-04-09 20:06:32 +02:00
RePixelatedMC 1eec3a09c1 now it should work 2024-04-09 18:05:27 +02:00
RePixelatedMC 60d349dd8b test 2024-04-09 17:05:43 +02:00
RePixelatedMC 0c8d144ffb Merge remote-tracking branch 'VolmitDev/smartHeight' into PixelatedDev 2024-04-09 17:04:45 +02:00
RePixelatedMC b898f73a05 Fixed another potential mem leak
Added the base of smart320height
2024-04-09 17:04:32 +02:00
CrazyDev22 c2b9b0ba0e add smartVanillaHeight 2024-04-09 16:52:14 +02:00
RePixelatedMC 206a4e9057 Revert "sync laptop"
This reverts commit 63b8a935ae.
2024-04-06 17:07:14 +02:00
RePixelatedMC 83a0a7dd54 sync laptop 2024-04-06 17:06:06 +02:00
RePixelatedMC 63b8a935ae sync laptop 2024-04-05 18:43:30 +02:00
CrazyDev22 16affd11cc fix HMCLeaves leave decay 2024-04-04 23:46:14 +02:00
CrazyDev22 33b0dec9da fix ClassCastException when replacing to IrisBlockData 2024-04-04 23:45:25 +02:00
Brian Neumann-Fopiano 121a463788 v+
Woot
2024-04-03 03:03:32 -04:00
Julian Krings 3625e70948 Merge pull request #1085 from VolmitDev/impl_mmoitem
Impl MMOItems
2024-04-02 19:03:25 +02:00
CrazyDev22 3d90207172 make MMOItems getItemTypes synchronous to reduce lag 2024-04-02 12:03:00 +02:00
CrazyDev22 ca7dc19c81 make mmoitems item creation synchronous 2024-04-01 23:20:11 +02:00
CrazyDev22 d0f16dbbc4 fix using Type::toString instead of Type::getId for mmoitems 2024-04-01 23:18:41 +02:00
CrazyDev22 fd91c223b7 add unload debug tools 2024-04-01 22:08:28 +02:00
CrazyDev22 93690b766c implement MMOItemsDataProvider 2024-04-01 17:26:33 +02:00
CrazyDev22 cc4af9950b fix scanning for item provider instead of block provider to processUpdate 2024-04-01 17:26:10 +02:00
Brian Fopiano a6281483dd Merge pull request #1084 from RePixelatedMC/PixelatedDev
Pixelated dev
2024-03-31 12:45:06 -04:00
RePixelatedMC f587816b30 merges 2024-03-31 18:33:35 +02:00
RePixelatedMC eea3c3ab0b Merge remote-tracking branch 'origin/master' into PixelatedDev
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/nms/INMSBinding.java
#	nms/v1_19_R1/src/main/java/com/volmit/iris/core/nms/v1_19_R1/NMSBinding.java
#	nms/v1_19_R2/src/main/java/com/volmit/iris/core/nms/v1_19_R2/NMSBinding.java
#	nms/v1_19_R3/src/main/java/com/volmit/iris/core/nms/v1_19_R3/NMSBinding.java
#	nms/v1_20_R1/src/main/java/com/volmit/iris/core/nms/v1_20_R1/NMSBinding.java
#	nms/v1_20_R2/src/main/java/com/volmit/iris/core/nms/v1_20_R2/NMSBinding.java
#	nms/v1_20_R3/src/main/java/com/volmit/iris/core/nms/v1_20_R3/NMSBinding.java
2024-03-31 18:25:26 +02:00
RePixelatedMC ba68ab9e06 f 2024-03-31 17:58:47 +02:00
RePixelatedMC eb94d97ea4 - Fixed caves breaksurface
- changes
2024-03-31 17:53:10 +02:00
Brian Neumann-Fopiano 04ad02719c d 2024-03-30 18:48:00 -04:00
Brian Fopiano f68f25b92f Merge pull request #1083 from VolmitDev/fix_object_collision_check
fix object collision check
2024-03-30 17:51:40 -04:00
Brian Fopiano 767cca36c8 Merge pull request #1080 from svdgoor/no-camel-1-19-R3
Cancels Camel spawns on 1-19-R3
2024-03-30 17:50:44 -04:00
Brian Fopiano b30172fe89 Merge pull request #1082 from VolmitDev/shut_up_spigot
Remove usage of System.out
2024-03-30 17:49:31 -04:00
CrazyDev22 b2934b0cc2 fix object collision check 2024-03-27 19:02:20 +01:00
RePixelatedMC 50686795d0 - Added /iris what region
- Sort of Fixed EngineStatus
- Added /iris hasAccess
- Added /iris irisworlds/worlds
- Added Some dev commands
- Redid / improved the mantle tectonic unload
- Convert still not working so ignore that
- Cleaned Iris of IrisPackBenchmarking.java im redoing it on second thought.
- Adjusted build.gradle
- Renamed some tectonic stuff
- Fixed Eta
- Fixed entities spawning inside blocks or at least it should.
- Lib update fixes all the iris titles/actionbars
- Cave spawning adjustments, now they get engine height if caveStartHeight is too large. <optional>
- Perhaps more?
2024-03-20 11:32:44 +01:00
CrazyDev22 5561a4dc2f fix wrong color bug 2024-03-20 08:46:56 +01:00
CrazyDev22 15b8780e17 remove usage of System.out 2024-03-20 08:44:00 +01:00
svdgoor 15c4d312cb Merge remote-tracking branch 'origin/master' 2024-03-18 17:29:02 +01:00
svdgoor ccbea89253 prevent spawning camels on 1.19.R3 2024-03-18 17:28:08 +01:00
RePixelatedMC 7b1e666b3b - No studio tectonic clearing. 2024-03-18 11:22:35 +01:00
RePixelatedMC 8af212fca8 - Fixed entities spawning inside blocks or at least it should. 2024-03-17 17:55:08 +01:00
Brian Fopiano 352ee7a622 Merge pull request #1079 from VolmitDev/better_plugin_compat
Better plugin compat
2024-03-17 02:55:19 -04:00
Brian Fopiano f26d0fac0b Merge pull request #1078 from VolmitDev/fix_v1_19_R3
Remove Cherry Grove from 1.19.4 biomes list
2024-03-17 02:55:03 -04:00
RePixelatedMC 9612fef2a4 - Added /iris what region
- Sort of Fixed EngineStatus
- Added /iris hasAccess
- Added /iris irisworlds/worlds
- Added Some dev commands
- Redid / improved the mantle tectonic unload
- Convert still not working so ignore that
- Cleaned Iris of IrisPackBenchmarking.java im redoing it on second thought.
- Adjusted build.gradle
- Renamed some tectonic stuff
- Perhaps more?
2024-03-16 12:40:40 +01:00
CrazyDev22 a691d49abc fix only the 0 0 chunk being updated 2024-03-08 19:35:07 +01:00
CrazyDev22 289eca35ec fix missing method parameter types 2024-03-07 19:41:28 +01:00
CrazyDev22 e9ca30257c woops 2024-03-07 18:48:09 +01:00
Julian Krings 6b21997baa Merge branch 'VolmitSoftware:master' into better_plugin_compat 2024-03-07 17:24:59 +01:00
CrazyDev22 6d3dbf84ef add HMCLeaves compatibility 2024-03-07 17:24:09 +01:00
CrazyDev22 eb45339c81 add oraxen furniture support 2024-03-07 17:23:32 +01:00
CrazyDev22 65e3fdd26c make implementing blocks that need world context possible 2024-03-07 17:23:00 +01:00
RePixelatedMC 34cad85942 - Fixed Eta 2024-03-01 12:32:53 +01:00
RePixelatedMC e407679d2b - Improved eta calculation 2024-03-01 11:44:00 +01:00
Julian Krings 8a08a3e148 Remove Cherry Grove from 1.19.4 biomes list 2024-02-27 11:30:40 +01:00
Brian Fopiano a01d27d273 Merge pull request #1075 from VolmitDev/fix_pregen
Fix Pregen oversight
2024-02-22 18:42:16 -05:00
Brian Fopiano 43f642ae8b Merge pull request #1076 from VolmitDev/custom-spawn-reason
Add implement custom spawn reason
2024-02-22 18:41:59 -05:00
CrazyDev22 93c1265de9 Add implement custom spawn reason 2024-02-22 21:25:37 +01:00
CrazyDev22 d08600fbaa Fix Pregen oversight 2024-02-22 17:27:38 +01:00
RePixelatedMC fb9f8d28f4 - Lib update fixes all the iris titles/actionbars
- Iris .schem Converter
2024-02-21 21:24:35 +01:00
Brian Fopiano cd3e9f772d Merge pull request #1074 from MoritzR200/master
Fixed file separators in '/iris import'
2024-02-17 09:54:59 -05:00
Brian Fopiano 7a95441799 Merge pull request #1071 from VolmitDev/fix_nullpointer
Fix nullpointer
2024-02-17 09:54:40 -05:00
Brian Fopiano 9345828c97 Merge pull request #1073 from RePixelatedMC/PixelatedDev
Fixes?
2024-02-17 09:54:14 -05:00
MoritzR200 58d88dd079 Fixed backslashes being used as file separators regardless of platform (in /iris import)). 2024-02-17 13:31:00 +01:00
RePixelatedMC 6cbc2374b3 whoopsies 2024-02-15 20:43:29 +01:00
RePixelatedMC 12bd11386d Merge remote-tracking branch 'CrazyDev/jigsaw_placement_fix' into PixelatedDev 2024-02-14 09:13:37 +01:00
RePixelatedMC 0497045388 - Fixed /Iris height when ran in console
- Added Dynamic worldHeight
2024-02-13 08:51:48 +01:00
Julian Krings d3f1640855 Merge branch 'VolmitSoftware:master' into jigsaw_placement_fix 2024-02-12 21:30:19 +01:00
CrazyDev22 960e2fcc61 try again if engine setup fails 2024-02-12 11:43:21 +01:00
CrazyDev22 6a012cf400 fix setup being set before engine setup 2024-02-12 11:42:47 +01:00
Brian Neumann-Fopiano d658ec2099 oops 2024-02-04 17:37:00 -05:00
Brian Fopiano c9a07bd9d7 Merge pull request #1069 from RePixelatedMC/PixelatedDev
This should be the last pr for the update
2024-01-26 07:35:39 -05:00
RePixelatedMC c0136585e6 Merge remote-tracking branch 'origin/master' 2024-01-26 10:59:14 +01:00
RePixelatedMC dc4cfc49ad Merge remote-tracking branch 'origin/master' into PixelatedDev 2024-01-26 10:58:48 +01:00
RePixelatedMC 9fc72868d7 disabled perms check 2024-01-26 10:57:03 +01:00
Brian Fopiano 7d4c9d6c1e Merge pull request #1066 from VolmitDev/better-version-compat
Better version compat
2024-01-25 10:51:56 -05:00
Brian Fopiano e68b1a52ee Merge pull request #1067 from RePixelatedMC/PixelatedDev
Feature and fIxes!
2024-01-25 10:51:45 -05:00
RePixelatedMC 955e8622b5 Merge remote-tracking branch 'VolmitDev/better-version-compat' into PixelatedDev 2024-01-25 14:57:47 +01:00
Julian Krings 82640de06b v+ 2024-01-25 14:47:19 +01:00
RePixelatedMC c93cb19563 - !Fixed pregen skipping chunks
- !Improved stacking for partof
- improved DeepSearch
- Some SFG Fixes
- Added some Todos
- Improvements to objects being placed in the air
- Studio BoardSVC for debug
2024-01-25 11:23:38 +01:00
Julian Krings ccb7855d7d Merge branch 'VolmitSoftware:master' into jigsaw_placement_fix 2024-01-20 15:10:05 +01:00
RePixelatedMC bad547356c Disabled for now 2024-01-14 16:43:39 +01:00
RePixelatedMC 1740bb0021 whoop 2024-01-14 16:35:00 +01:00
RePixelatedMC 20f05bf317 whoop 2024-01-14 16:32:15 +01:00
RePixelatedMC 669ede5482 remove turbo for now 2024-01-14 10:18:41 +01:00
Julian Krings ab658f58f9 Merge branch 'VolmitSoftware:master' into better-version-compat 2024-01-13 10:52:50 +01:00
Brian Fopiano 1b3916749a Merge pull request #1063 from RePixelatedMC/PixelatedDev
Quite some more fixes
2024-01-12 10:38:06 -05:00
RePixelatedMC e29ec657d3 e 2024-01-12 16:10:02 +01:00
RePixelatedMC 04f3a226a1 whoop 2024-01-12 16:07:02 +01:00
RePixelatedMC 099d1cc362 - Added DeepSearchPregenerator.java < although not finished the base is functional >
- Fixes!
- Dynmap SFG detection now works!
2024-01-12 15:56:04 +01:00
RePixelatedMC c11e9bf704 Merge remote-tracking branch 'origin/master' into PixelatedDev 2024-01-11 22:56:48 +01:00
RePixelatedMC ffa0b984a0 whoop 2024-01-10 18:47:14 +01:00
RePixelatedMC f0f9261ed3 whoop 2024-01-04 15:29:23 +01:00
RePixelatedMC ba433bc244 - Display true height instead of engine height option added! 2024-01-04 15:26:25 +01:00
Julian Krings 27f8ad982a Merge branch 'VolmitSoftware:master' into jigsaw_placement_fix 2023-12-31 19:15:39 +01:00
CrazyDev22 9b89ae7846 reimplement IrisCompat 2023-12-31 19:13:57 +01:00
Brian Fopiano c11b5342e3 Merge pull request #1062 from RePixelatedMC/master
Fixed a rare crash - Crazydev
2023-12-30 08:56:53 -05:00
RePixelatedMC b9c88f5b12 Fixed a rare crash - Crazydev 2023-12-30 14:36:07 +01:00
RePixelatedMC 02c7ab71d4 Merge remote-tracking branch 'PixelsV3/PixelatedDev' into PixelatedDev
# Conflicts:
#	core/src/main/java/com/volmit/iris/core/pregenerator/TurboPregenerator.java
2023-12-30 14:31:22 +01:00
RePixelatedMC 8ec1ac9875 sync 2023-12-30 14:29:31 +01:00
RePixelatedMC e03027a386 Now using klist for tounload 2023-12-30 14:15:59 +01:00
Julian Krings 387cf566c9 Merge branch 'VolmitSoftware:master' into jigsaw_placement_fix 2023-12-30 11:25:33 +01:00
Brian Fopiano dff7665934 Merge pull request #1053 from RePixelatedMC/Pixeldev
Pixeldev
2023-12-29 16:32:21 -05:00
RePixelatedMC 9421d788f3 Backup 2023-12-28 17:56:30 +01:00
RePixelatedMC d960574b24 Ah yes 2023-12-28 14:41:16 +01:00
RePixelatedMC e8671cbbf0 whoop 2023-12-27 09:13:14 +01:00
RePixelatedMC cbf8039eea Cleanup p2 2023-12-27 09:09:11 +01:00
RePixelatedMC d7415e3711 Merged CommandWorldManager.java into CommandIris.java 2023-12-27 09:05:05 +01:00
RePixelatedMC 26e71ee6b5 small tweak 2023-12-27 08:06:40 +01:00
RePixelatedMC f8c42b3e48 cleaning up 2023-12-27 07:35:24 +01:00
RePixelatedMC 886bed9634 o 2023-12-26 13:38:31 +01:00
RePixelatedMC 7b7023fc99 Added ToUpgradedVersion 2023-12-26 13:32:37 +01:00
RePixelatedMC 701bfe4a54 Merge pull request #10 from CrazyDev05/Pixeldev
Fix lastUse and Implement MultiBurst
2023-12-25 14:14:16 +01:00
CrazyDev22 290d7a93ab switch to burst 2023-12-25 13:53:45 +01:00
CrazyDev22 729bcc7ba0 fix lastUse 2023-12-25 13:52:31 +01:00
RePixelatedMC e46b68e2f3 comment 2023-12-24 16:41:20 +01:00
RePixelatedMC cdf73c4629 Nvmr 2023-12-24 16:40:44 +01:00
RePixelatedMC 2f0dd2d172 Quick fix 2023-12-24 16:17:41 +01:00
RePixelatedMC 7e5704bf80 No idea why crazy recommended this 2023-12-24 15:17:56 +01:00
RePixelatedMC ef96fb9a0e Removed debug code 2023-12-24 15:06:53 +01:00
RePixelatedMC f2370ad814 Merge pull request #9 from CrazyDev05/Pixeldev
Sync with Master
2023-12-24 15:03:24 +01:00
RePixelatedMC 0783620104 Way to tired 2023-12-24 15:00:02 +01:00
RePixelatedMC 2193b02c0a Removed debug code 2023-12-24 14:40:41 +01:00
RePixelatedMC aef05bad20 Deleted leftovers and a couple of changes 2023-12-24 14:32:13 +01:00
CrazyDev22 e002ab6378 disable 1.20.4 nms bindings for now 2023-12-23 21:17:35 +01:00
Julian Krings a2c2b4a2d4 Sync with Master 2023-12-23 20:53:33 +01:00
Julian Krings 3ae896457b Merge branch 'Pixeldev' into origin 2023-12-23 20:53:18 +01:00
Julian Krings 683a90ed09 Merge branch 'VolmitSoftware:master' into jigsaw_placement_fix 2023-12-23 20:52:13 +01:00
Brian Fopiano 5685e25234 Merge pull request #1061 from CrazyDev05/build_fix
fix missing dependency
2023-12-23 14:43:07 -05:00
CrazyDev22 6edf083e7b fix <world>/iris/iris/pack path 2023-12-23 20:39:54 +01:00
CrazyDev22 995d1495eb fix missing dependency 2023-12-23 20:14:43 +01:00
Julian Krings d5f74631cd Merge branch 'VolmitSoftware:master' into jigsaw_placement_fix 2023-12-23 18:37:59 +01:00
Brian Fopiano dd5b9b2e81 Merge pull request #1060 from CrazyDev05/build_fix
Build fix
2023-12-23 12:32:09 -05:00
RePixelatedMC ca78200c1c Deleted leftovers 2023-12-23 15:01:27 +01:00
RePixelatedMC d73c2a65ae - Saving iris and mc version data to engine 2023-12-23 14:57:33 +01:00
CrazyDev22 0ee2cb8e3c why did I add that 2023-12-23 11:47:26 +01:00
CrazyDev22 5ba429ec73 implement getBiomes method 2023-12-23 11:40:06 +01:00
CrazyDev22 709825d073 decrease build time to ~30s
revert single version building
2023-12-23 11:39:40 +01:00
Julian Krings 3fe73af5e8 Merge branch 'VolmitSoftware:master' into jigsaw_placement_fix 2023-12-23 10:20:30 +01:00
Brian Fopiano ccc0b0dd7d Merge pull request #1057 from CrazyDev05/single_nms
add ability to only compile for one mc version
2023-12-22 20:03:58 -05:00
Brian Fopiano 1114ede267 Merge pull request #1058 from IkyMax/master
Update to 1.20.4
2023-12-22 20:03:37 -05:00
Brian Fopiano 5546f12499 Merge pull request #1052 from CrazyDev05/worldedit_fix
Worldedit fix
2023-12-22 20:03:24 -05:00
Brian Fopiano 4f38acb592 Merge pull request #1055 from CrazyDev05/multiversion_fix
NoSuchFieldError: CHERRY_GROVE fix
2023-12-22 20:03:11 -05:00
RePixelatedMC 39f59e023c Merge pull request #7 from CrazyDev05/Pixeldev
implement showing location instead of teleporting
2023-12-22 21:36:42 +01:00
CrazyDev22 5ed5322fce implement showing location instead of teleporting 2023-12-22 21:23:01 +01:00
RePixelatedMC 33b926d547 Merge pull request #6 from CrazyDev05/Pixeldev
Fixes!
2023-12-22 19:54:54 +01:00
CrazyDev22 af8778c6cd fix ArrayIndexOutOfBoundsException
fix NullPointerException
fix ConcurrentModificationException x2
2023-12-22 19:36:07 +01:00
RePixelatedMC bb4ad08cc5 Merge pull request #5 from CrazyDev05/Pixeldev
More Improvements
2023-12-22 16:39:30 +01:00
CrazyDev22 ee4eb7b3f0 fix 2023-12-22 16:34:39 +01:00
CrazyDev22 5bc024a6ee Merge remote-tracking branch 'pixeldev/Pixeldev' into Pixeldev 2023-12-22 16:33:51 +01:00
RePixelatedMC 0546caeb05 - Fixed Process Threads displaying the incorrect value on startup. 2023-12-22 16:24:48 +01:00
CrazyDev22 a75348d3ee Merge remote-tracking branch 'pixeldev/Pixeldev' into Pixeldev 2023-12-22 16:19:49 +01:00
CrazyDev22 3f3f947f43 change the Param description 2023-12-22 16:17:56 +01:00
RePixelatedMC 5127b7c959 - Added DiskSpace to Splash
- Changed form ofSize to contain a space
2023-12-22 16:16:48 +01:00
CrazyDev22 a28df78792 switch to lz4 block compression 2023-12-22 16:13:57 +01:00
CrazyDev22 4f888f16bc Add Compression Test Command 2023-12-22 16:13:46 +01:00
RePixelatedMC 55ed9d7132 - Added DiskSpace check 2023-12-22 15:59:05 +01:00
CrazyDev22 779a56c3a1 add compression test command 2023-12-22 15:50:27 +01:00
RePixelatedMC f6440bde07 - Added Sufficient Permission check 2023-12-22 15:33:22 +01:00
RePixelatedMC 930dc2fd1c Merge pull request #4 from CrazyDev05/Pixeldev
increase unload speed
2023-12-22 11:48:23 +01:00
CrazyDev22 1e96fd529b increase unload speed 2023-12-22 11:17:55 +01:00
RePixelatedMC 167c865bb2 e 2023-12-22 09:00:57 +01:00
RePixelatedMC 4e6b6c11a3 Merge remote-tracking branch 'PixelsV3/Pixeldev' into Pixeldev
# Conflicts:
#	core/src/main/java/com/volmit/iris/util/mantle/Mantle.java
2023-12-22 08:37:27 +01:00
RePixelatedMC bfdb71ca91 Merge pull request #3 from CrazyDev05/Pixeldev
TectonicPlate unloading improvements
2023-12-22 08:35:35 +01:00
IkyMax f590f9824e Update to 1.20.4 2023-12-21 15:58:45 -06:00
CrazyDev22 b3a087702d show unloaded tectonicplates count 2023-12-21 22:36:50 +01:00
CrazyDev22 1a786fb418 give hyperLock a purpose again 2023-12-21 22:36:05 +01:00
CrazyDev22 650d38e212 Implement MultiThreading in the TectonicPlates unloader 2023-12-21 22:30:08 +01:00
RePixelatedMC 130cb7ad93 So that works 2023-12-21 22:04:19 +01:00
CrazyDev22 85e8ffeaa3 Merge remote-tracking branch 'pixeldev/Pixeldev' into Pixeldev 2023-12-21 20:55:37 +01:00
RePixelatedMC db40f608fa try this out 2023-12-21 20:55:10 +01:00
RePixelatedMC a7b471682a try this out 2023-12-21 15:39:26 +01:00
CrazyDev22 4595fea7d7 fix NullPointerException 2023-12-21 12:34:14 +01:00
CrazyDev22 4580f8d9a4 fix StackOverflowError 2023-12-21 12:21:20 +01:00
CrazyDev22 37f76fa133 Dynamically define tectonicLimit and cooldown 2023-12-20 14:43:37 +01:00
RePixelatedMC 5507c35547 new structure 2023-12-17 17:42:28 +01:00
CrazyDev22 350328f99c fix remapping not working 2023-12-16 18:40:53 +01:00
CrazyDev22 91ec6b773d add ability to only compile for one mc version 2023-12-16 17:22:45 +01:00
RePixelatedMC c0fdf23133 cleanup 2023-12-16 14:32:06 +01:00
RePixelatedMC 28523a9593 e 2023-12-16 14:29:56 +01:00
RePixelatedMC 07e0b52ea3 e 2023-12-16 14:08:10 +01:00
RePixelatedMC 008e758da8 To many issues that are just not worth dealing with 2023-12-16 13:51:31 +01:00
RePixelatedMC 7ce219a6a8 - Added warning mode
- ignoreUnstable Changed to ignoreBootMode
2023-12-16 13:45:21 +01:00
RePixelatedMC 1809d4a53f - Added JDK version check for an SFG
- Fixed duplicate cpu message on boot when on intel and unstable
2023-12-15 20:14:34 +01:00
RePixelatedMC 54ae026c2b - Fixed LazyPregenerator.java and improved it. ( Speed decrease on server restart when lazy starts the job up dont know why)
- Mem leak fix only works when there is 1 iris world for now ( working on it ) + A capped performance limit still very alpha like seems stable tho.

- Disabled HotDropWorldSVC acts a bit weird sometimes.
2023-12-15 19:06:53 +01:00
RePixelatedMC 57c647fa0e Whoops 2023-12-07 18:09:36 +01:00
RePixelatedMC 9745f23fb2 - Saving the LazyPregen Progress in world folder
- Added option to pause to LazyPregen
- Added option to stop to LazyPregen
- Re-Added the functionality continue pregen on startup
2023-12-07 17:57:45 +01:00
RePixelatedMC 7d2062f298 e 2023-12-07 16:06:33 +01:00
RePixelatedMC 306a1948cf - Moved iris regen to studio
- Moved Iris LazyPregen into its own cmd
- Fixed the mem leak "i hope"
- Added Silent option to the lazy pregen
2023-12-07 16:04:40 +01:00
RePixelatedMC ac4c00d3f2 Thanks Crazydev! 2023-12-07 14:27:08 +01:00
RePixelatedMC db43bc300e Sync 2023-12-07 11:26:14 +01:00
RePixelatedMC 6494cded62 Sync 2023-12-07 10:41:17 +01:00
RePixelatedMC b5c1e7e2da Sync 2023-12-06 15:56:19 +01:00
RePixelatedMC 83e311870f Sync 2023-12-03 16:56:56 +01:00
RePixelatedMC 8fc6e3b335 Dev code + added maxcpm for lazy gen 2023-12-03 12:45:36 +01:00
RePixelatedMC 18a69e97b3 not really lazy but oh well at least it fixed a crash 2023-11-30 10:19:57 +01:00
RePixelatedMC 80d8449f02 Bug when on intel cpu and unstable mode and changed how SFG works 2023-11-29 18:04:56 +01:00
RePixelatedMC d830aa1d1a Bug when on intel cpu and unstable mode and moved more to SFG 2023-11-29 16:26:34 +01:00
RePixelatedMC d473126d4c What was this even meant to do? 2023-11-29 16:16:11 +01:00
RePixelatedMC 17f8fe69fb so I didnt know that 2023-11-29 14:43:06 +01:00
Julian Krings 7263d102c7 cleanup / possibly fix a bug 2023-11-29 14:34:00 +01:00
RePixelatedMC 815b2235a2 FINALLY 2023-11-29 13:48:58 +01:00
RePixelatedMC f61247f8bb Raw data 2023-11-29 11:31:26 +01:00
RePixelatedMC 205d9d528e back to debug 2023-11-29 10:47:45 +01:00
RePixelatedMC 1044f901bd o 2023-11-29 10:41:48 +01:00
RePixelatedMC de261a38fd o 2023-11-29 10:39:48 +01:00
RePixelatedMC 81776e60fb So now it actually it works 2023-11-29 10:31:44 +01:00
RePixelatedMC 0c179eae86 e 2023-11-26 19:42:25 +01:00
RePixelatedMC c30de98449 e 2023-11-26 19:02:50 +01:00
RePixelatedMC 47990cde46 Thanks @CrazyDev! 2023-11-24 20:44:56 +01:00
Julian Krings b098c210ce removed unnecessary code 2023-11-24 17:03:32 +01:00
RePixelatedMC 6edd8bf119 Ah 2023-11-24 15:55:17 +01:00
RePixelatedMC de0e124ebb For fuck's sake, why haven't I done this earlier 2023-11-24 15:38:12 +01:00
RePixelatedMC 5fc012a557 no way im spending more time on something stupid as this 2023-11-24 15:27:05 +01:00
RePixelatedMC 32afcc3478 - Fixed hotdrop being activated on world creation
- Improved /iris delete
- Unfinished code
2023-11-24 15:21:15 +01:00
RePixelatedMC a27f37945d Merge branch 'VolmitSoftware:master' into Pixeldev 2023-11-24 14:26:06 +01:00
RePixelatedMC 0adfe6911f smth like this 2023-11-24 13:08:42 +01:00
RePixelatedMC 8bb1964b30 smth like this 2023-11-23 16:49:45 +01:00
RePixelatedMC cc95e1ae63 More more 2023-11-23 16:47:55 +01:00
Julian Krings 9843f18c96 Fix jigsaw piece collision/merging 2023-11-22 13:45:55 +01:00
Julian Krings 18ea61fcc0 Move Biome List for DummyBiomeProvider into NMSBindings 2023-11-21 12:50:54 +01:00
Julian Krings 5b3fdb6e62 Optimise WorldEdit check 2023-11-17 15:43:59 +01:00
RePixelatedMC c5220c8d06 Fix for HotDropWorldSVC.java not working 2023-11-17 12:20:47 +01:00
RePixelatedMC 74e87a7fae Fix for HotDropWorldSVC.java not working 2023-11-17 12:17:46 +01:00
Julian Krings c809f50c3b Optimise check for wand item 2023-11-17 11:49:23 +01:00
Julian Krings e7e46f78c7 Disable WorldEditLink when WorldEdit not present/loaded 2023-11-17 11:41:57 +01:00
Julian Krings 2b449a1f3d Add Settings check before calling WorldEditLink 2023-11-17 11:40:56 +01:00
Brian Neumann-Fopiano f257f2c551 updated my mac path 2023-11-15 15:47:40 -05:00
Brian Fopiano 7f91d73d27 Merge pull request #1051 from RePixelatedMC/Pixeldev
Whoops
2023-11-15 12:24:00 -05:00
RePixelatedMC 15a90edb03 More robust and some debugging 2023-11-15 18:06:54 +01:00
RePixelatedMC b2f1e16d9a more aggressive 2023-11-15 16:56:34 +01:00
RePixelatedMC f544455708 Merge branch 'VolmitSoftware:master' into Pixeldev 2023-11-15 16:38:43 +01:00
RePixelatedMC 5118997188 Whoop 2023-11-15 16:37:34 +01:00
Brian Fopiano b6ae649cd1 Merge pull request #1050 from RePixelatedMC/Pixeldev
Mem leak Fix + more
2023-11-15 10:02:52 -05:00
RePixelatedMC a6d0d85b84 deleted unused class 2023-11-15 12:48:26 +01:00
RePixelatedMC c71c2494af sync 2023-11-15 12:22:36 +01:00
RePixelatedMC 4a3a7bc98d Whoop 2023-11-15 11:16:33 +01:00
RePixelatedMC 4e4f7f693d Sync 2023-11-15 11:14:26 +01:00
RePixelatedMC 7a729c39a6 e 2023-11-15 09:58:46 +01:00
RePixelatedMC 48b757f693 Forgot those 2023-11-15 08:43:38 +01:00
RePixelatedMC b449938730 Merge branch 'VolmitSoftware:master' into Pixeldev 2023-11-15 08:18:39 +01:00
RePixelatedMC ba55ff6a4b typo 2023-11-12 10:19:39 +01:00
RePixelatedMC 2b4bc621ac misc 2023-11-11 21:56:28 +01:00
RePixelatedMC acac2d0fc5 Removed GPT 4 Comments lmao 2023-11-11 21:26:31 +01:00
RePixelatedMC 6ff5975918 - HotDrop iris worlds lmao 2023-11-11 21:25:24 +01:00
RePixelatedMC 8912cfe37b fixed declaration 2023-11-11 16:14:37 +01:00
RePixelatedMC 9e02c318a1 Refactoring and a new pregen message 2023-11-11 09:50:24 +01:00
Brian Fopiano f6b51c21d5 Merge pull request #1049 from RePixelatedMC/Pixeldev
[ Ready ] SAFEGUARD
2023-11-06 17:21:55 -05:00
RePixelatedMC df3e4b055f Merge branch 'master' into Pixeldev 2023-11-06 17:30:18 +01:00
RePixelatedMC ffcd2b4179 z 2023-11-06 15:22:23 +01:00
RePixelatedMC 7afdbc2a53 - Added Lazy pregen as an option 2023-11-06 15:03:40 +01:00
RePixelatedMC 5004481685 - Fixed /iris remove for real this time lol
- Changed it back to /iris instead of /iris worldmanager for easier access
2023-11-04 20:04:05 +01:00
RePixelatedMC 1905741a7c Matched the CPU Brand color theme lol 2023-11-04 17:30:22 +01:00
RePixelatedMC 8bf7451107 - Removed debug code
- fixed unstableprompt
2023-11-04 17:00:09 +01:00
RePixelatedMC fdfb708ce0 Removed FixUnstable CMD for now.. 2023-11-04 16:43:10 +01:00
Brian Fopiano 68a4eee8bb Merge pull request #1047 from CrazyDev05/executable_items
add ExecutableItems Implementation
2023-11-04 11:17:14 -04:00
Brian Fopiano 746ebfffbd Merge pull request #1044 from CrazyDev05/object_fix
Object Spawning pattern fix
2023-11-04 11:16:44 -04:00
Brian Fopiano 213f8645bc Merge pull request #1045 from CrazyDev05/papi_fix
Fix PapiExpansion not persisting after papi reload
2023-11-04 11:16:14 -04:00
Brian Fopiano 1b2541bd84 Merge pull request #1043 from CrazyDev05/build_fix
build.gradle fix
2023-11-04 11:15:57 -04:00
RePixelatedMC 522de89792 Fixed Crash on lower end servers + Cleanup 2023-11-04 14:35:40 +01:00
RePixelatedMC 5feaa45edf cleanup 2023-11-03 10:20:59 +01:00
RePixelatedMC 9333775e0e wait no 2023-11-03 09:23:28 +01:00
RePixelatedMC afd15ad450 A lot of cleanup 2023-11-03 09:21:59 +01:00
RePixelatedMC e7fc9e6fe5 Test 2023-11-03 08:28:19 +01:00
RePixelatedMC 021f9b1d0e Cleanup / Fixes 2023-11-02 16:55:47 +01:00
RePixelatedMC dc21f05482 Cleanup 2023-11-02 13:42:50 +01:00
RePixelatedMC c22ccf71a7 Added setting option for ChunkHandler.java and no more random freezes. 2023-11-02 13:03:09 +01:00
RePixelatedMC d5b9a074e6 Additions to ChunkHandler.java 2023-11-02 12:53:30 +01:00
RePixelatedMC 63e6104736 EXPERIMENTAL ChunkHandler.java 2023-11-02 12:24:13 +01:00
RePixelatedMC de475c9561 Changes 2023-11-01 16:02:53 +01:00
RePixelatedMC 3f4fa0c67c Disabled Pack Benchmark for now.. 2023-11-01 15:29:17 +01:00
RePixelatedMC ca3f228b89 Cleaning and idi- i mean user issue prevention system. ALPHA 2023-10-29 20:53:56 +01:00
RePixelatedMC 2b159041ac Cleaned SFG a bit 2023-10-29 14:14:37 +01:00
RePixelatedMC 5fa564969e Added cmd to see what is causing the instability 2023-10-29 14:09:25 +01:00
RePixelatedMC 8ba8627281 Improved pack Benchmarking not done yet.. 2023-10-28 20:51:32 +02:00
RePixelatedMC a12cb59a51 Changed how ServerBootSFG Works Part1.
Added More warnings and Checks.
Improved Unstable Colored text.
Text for console and Player errors when in unstable.
2023-10-28 20:49:47 +02:00
RePixelatedMC dd124fab33 Changed to RED Iris tag when its unstable. 2023-10-28 19:33:16 +02:00
BuildTools 9f8ef4c1f3 add ExecutableItems Implementation 2023-10-27 21:25:46 +02:00
BuildTools 563c6d0403 make object collision configurable 2023-10-26 20:22:34 +02:00
RePixelatedMC 1bad0fe048 test 2023-10-26 19:46:38 +02:00
RePixelatedMC b40dcd3164 fixup! Pack Benchmarking - Iris pregen is broken what it relies on so it doesnt work. Issue: Generated and total chunks dont end up as the same value after the pregen is done 2023-10-26 17:52:33 +02:00
RePixelatedMC 8a0b443d28 Pack Benchmarking
- Iris pregen is broken what it relies on so it doesnt work.
Issue: Generated and total chunks dont end up as the same value after the pregen is done
2023-10-26 17:28:43 +02:00
Julian Krings 5f38503ac5 Fix PapiExpansion not persisting after papi reload 2023-10-24 17:43:36 +02:00
BuildTools 231d1209c9 make object spawning more random 2023-10-24 16:07:43 +02:00
RePixelatedMC 06bc180127 renames + dev sync 2023-10-24 15:16:39 +02:00
BuildTools fb2221fa8d fix iris task having the wrong source path 2023-10-23 16:11:40 +02:00
BuildTools e1c7f38bb8 re-add setup task 2023-10-23 15:46:21 +02:00
Brian Neumann-Fopiano a82fa578eb nana 2023-10-23 05:51:54 -04:00
Brian Fopiano 0ecefdcc1c Merge pull request #1041 from CrazyDev05/multi_version
Make Iris multi version compatible
2023-10-23 05:17:34 -04:00
CrazyDev22 8bacc8128b move base project to 'core'
add build script for nms
add build script for core
add build script for project
move existing nms references to nms/v1_20_R1
add 1.19 and 1.20.2 compatibility
2023-10-23 08:54:30 +02:00
Brian Neumann-Fopiano 3dff96152c v+ 2023-10-22 23:35:58 -04:00
Brian Fopiano 79e198d441 Merge pull request #1038 from RePixelatedMC/repixelatedmc
Repixelatedmc
2023-10-22 23:17:53 -04:00
Brian Fopiano e6cf33e262 Merge pull request #1040 from CrazyDev05/repo_fix
Fix build.gradle

Thanks
2023-10-22 23:17:26 -04:00
RePixelatedMC f839ca674b Remove / Delete Iris world actually works now lol 2023-10-22 20:08:58 +02:00
RePixelatedMC f9427d1258 ROTATION FIX WHOHOO 2023-10-22 15:22:01 +02:00
RePixelatedMC bd2cdc9bd0 idk some fix 2023-10-22 12:09:10 +02:00
RePixelatedMC 67352311de WHooPs 2023-10-22 11:02:46 +02:00
RePixelatedMC 555a7682e1 fixes 2023-10-22 11:01:07 +02:00
RePixelatedMC 19d94489d7 Merge pull request #2 from RePixelatedMC/repixelated-DEV
Repixelated dev
2023-10-22 10:58:00 +02:00
RePixelatedMC 0175cfa986 fixes 2023-10-22 10:54:12 +02:00
RePixelatedMC 1b0ff36d51 cleanup 2023-10-22 10:44:32 +02:00
RePixelatedMC f5c499ab60 Fixes + cleanup 2023-10-22 10:40:40 +02:00
RePixelatedMC 81e3fd346a Rewrote The update process and made it more manual 2023-10-22 10:13:18 +02:00
RePixelatedMC 3bca1f5304 Fixes? 2023-10-22 09:20:13 +02:00
RePixelatedMC 593e96a31a added WindowsCPUSpeedTest 2023-10-22 08:47:33 +02:00
Julian Krings 169b783e4f Fix build.gradle 2023-10-21 22:47:01 +02:00
RePixelatedMC f129742a41 cleanup 2023-10-21 20:19:03 +02:00
RePixelatedMC 01016d1b14 modes? 2023-10-21 20:18:02 +02:00
RePixelatedMC a883e43acb eh 2023-10-21 15:26:24 +02:00
RePixelatedMC e0d0657e7b thx coco 2023-10-21 15:25:55 +02:00
RePixelatedMC e4c27bd49c thx coco 2023-10-08 14:10:44 +02:00
RePixelatedMC 7a31fd31eb whoop 2023-10-08 14:01:28 +02:00
RePixelatedMC 4f0f2c3213 grammar fix 2 2023-10-08 14:00:57 +02:00
RePixelatedMC d9889b06ad grammar fix 2023-10-08 13:59:56 +02:00
RePixelatedMC 9f5cff4ab8 brr 2023-10-06 12:41:21 +02:00
RePixelatedMC 958543a171 brr 2023-10-06 12:31:34 +02:00
RePixelatedMC b8e03f5e13 Merge remote-tracking branch 'origin/master' 2023-10-06 12:28:40 +02:00
Brian Neumann-Fopiano e76218b39f forgot the overworld , this is the V+ 2023-10-06 12:13:36 +02:00
Brian Neumann-Fopiano 7d1a375bda v+ 2023-10-06 12:13:36 +02:00
CrazyDev22 dfaa80f1e4 Add Color Gradient 2023-10-06 12:13:36 +02:00
CrazyDev22 8939a903be Add NullablePlayerHandler
Add Teleport Command
2023-10-06 12:13:32 +02:00
CrazyDev22 d6f1ea78fa Fix NullPointerException 2023-10-06 12:13:14 +02:00
CrazyDev22 f25c4c74be Fix IrisJigsawPiece tectonic plate panic 2023-10-06 12:13:14 +02:00
BuildTools 1619df1ba3 remove debug info 2023-10-06 12:13:14 +02:00
BuildTools 566169cdbf better fix for jigsaw loot 2023-10-06 12:13:13 +02:00
RePixelatedMC ee3e631789 comment 2023-10-06 11:47:24 +02:00
RePixelatedMC 8202754988 remove world to worldmanager 2023-10-06 10:30:24 +02:00
RePixelatedMC 447d7596dc added evacuate 2023-10-06 10:26:43 +02:00
RePixelatedMC 7c4568066f IrisSafeguard Prototype idea
WorldManager ( Not done WIP )
2023-10-06 10:14:33 +02:00
RePixelatedMC 46ee7a5983 IrisSafeguard Prototype idea 2023-10-04 14:12:53 +02:00
Brian Neumann-Fopiano 5a8ef99afa forgot the overworld , this is the V+ 2023-10-04 03:30:55 -04:00
Brian Neumann-Fopiano 571729cfe4 v+ 2023-10-04 03:24:32 -04:00
Brian Fopiano c1de316ed6 Merge pull request #1036 from CrazyDev05/teleport
Add Teleport Command
2023-10-04 03:22:29 -04:00
Brian Fopiano 1e316acd64 Merge pull request #1037 from CrazyDev05/jigsaw_piece_mantle_panic_fix
Fix IrisJigsawPiece tectonic plate panic
2023-10-04 03:21:43 -04:00
CrazyDev22 f89460538d Fix NullPointerException 2023-10-03 14:20:40 +02:00
CrazyDev22 0bf0062c2c Fix IrisJigsawPiece tectonic plate panic 2023-10-01 22:28:03 +02:00
CrazyDev22 540d1b5801 Add Color Gradient 2023-10-01 17:45:40 +02:00
CrazyDev22 8f0d1b7b7b Add NullablePlayerHandler
Add Teleport Command
2023-10-01 16:50:50 +02:00
Brian Fopiano 5061791dcf Merge pull request #1034 from RePixelatedMC/master
Fixed - /iris object undo / Added - IrisLootEvent
2023-09-27 08:15:06 -04:00
Brian Fopiano ce98264552 Merge pull request #1033 from CrazyDev05/jigsaw_loot_fix_2
Better Jigsaw loot fix
2023-09-27 08:14:51 -04:00
RePixelatedMC 15e6750e11 Merge pull request #1
IrisLootEvent
2023-09-27 10:31:43 +02:00
RePixelatedMC 7a20421580 Added IrisLootEvent.java and some debugging to that 2023-09-27 10:15:11 +02:00
RePixelatedMC f42f06226c Optimized it thx coco 2023-09-25 15:12:45 +02:00
RePixelatedMC cce3d74c52 Merge remote-tracking branch 'origin/master' 2023-09-25 15:03:41 +02:00
RePixelatedMC 7b9fb880f4 Also changed the color for the /iris o paste cmd 2023-09-25 15:02:33 +02:00
RePixelatedMC 2a9b0a54d9 Merge branch 'VolmitSoftware:master' into master 2023-09-25 14:37:08 +02:00
RePixelatedMC 373e9e9755 Fixed - /iris object undo
also added some colors
2023-09-25 14:30:21 +02:00
BuildTools 4115dd5797 remove debug info 2023-09-18 13:48:19 +02:00
BuildTools 57bfd251dc better fix for jigsaw loot 2023-09-18 13:45:03 +02:00
Brian Neumann-Fopiano d324790f66 forgot the v+ lol 2023-09-17 15:54:08 -04:00
Brian Fopiano 99d3dba440 Merge pull request #1026 from CocoTheOwner/jigsawRotationFix
Disable initial rotation if set to true
2023-09-17 13:47:51 -04:00
Brian Fopiano 5a44e79ad1 Merge pull request #1022 from CrazyDev05/jigsaw_loot_fix
Jigsaw loot fix
2023-09-17 13:47:26 -04:00
CocoTheOwner 86808017db Disable initial rotation if set to true 2023-09-06 18:24:15 +02:00
CrazyDev22 f735db9843 Fix not finding IrisObjectPlacement for jigsaw structures 2023-09-02 10:31:34 +02:00
CrazyDev22 5a333c23ac Fix Exact BlockData Filter 2023-09-02 10:30:44 +02:00
Brian Neumann-Fopiano 4d2d51edfa v+ 2023-08-31 14:50:49 -04:00
Brian Fopiano dcfd5a0cd8 Merge pull request #1010 from theDAVID543/master
fix bug
2023-08-31 14:41:28 -04:00
theDAVID543 a821c0da50 fix bug 2023-08-18 11:53:06 +08:00
Brian Neumann-Fopiano 804147e2c4 overworld bump 2023-08-12 11:47:42 -04:00
Brian Neumann-Fopiano ed346291f9 oops v+ 2023-07-29 06:13:42 -04:00
Brian Fopiano 2190f6eff8 Merge pull request #1003 from CocoTheOwner/better-pregen-pred
Predict the ETA for pregeneration more rapidly
2023-07-29 03:04:07 -07:00
Brian Fopiano ce30e24e42 Merge pull request #1002 from CocoTheOwner/pregen-center-rescale
Fixes a scaling issue with pregen center
2023-07-29 03:03:27 -07:00
Brian Fopiano d5e13541c8 Merge pull request #1000 from christolis/fix-macos-build-error
Fix unresolved dependency error
2023-07-29 03:03:11 -07:00
Brian Fopiano 2e707adaa0 Merge branch 'master' into fix-macos-build-error 2023-07-29 03:03:00 -07:00
Brian Fopiano 4d8e36cb05 Update bug.yml 2023-07-24 04:34:17 -04:00
Brian Neumann-Fopiano 73b710cdde oraxen update 2023-07-16 18:46:31 -04:00
Brian Neumann-Fopiano 09182bf3f3 V+ for iris overworld tweaks 2023-07-13 07:09:09 -04:00
CocoTheOwner 9a945837cd Actually just after 1 region is generated switch 2023-07-10 23:09:02 +02:00
CocoTheOwner b64c821ef4 smooth into the warm-up generator 2023-07-10 23:00:28 +02:00
CocoTheOwner a9ab3bc519 Predict pregen better 2023-07-10 22:49:50 +02:00
CocoTheOwner 94292cea6e shift pregen center to blocks instead of regions 2023-07-10 18:43:49 +02:00
Christolis b5fdeb2a02 build: add extra dependency 2023-07-08 00:26:43 +03:00
Brian Neumann-Fopiano fa3ac36e8c v+ 2023-07-03 23:27:18 -04:00
Brian Neumann-Fopiano 6632d1b65f inc the git 2023-07-03 23:24:55 -04:00
Brian Neumann-Fopiano 8805693800 1.20.1 inital update 2023-06-13 12:18:48 -04:00
Brian Neumann-Fopiano 93db7ee0ee coco's other pr 2023-06-11 14:09:15 -04:00
Brian Fopiano 9ba4dd4099 Merge pull request #995 from CocoTheOwner/printWrongSpawner
Better print information about wrong spawner on marker
2023-06-11 14:00:09 -04:00
Brian Neumann-Fopiano 36efacf43c inc dim 2023-06-11 13:36:35 -04:00
Brian Neumann-Fopiano d9bf91afa5 1.20 2023-06-11 13:05:23 -04:00
Brian Neumann-Fopiano b0c96be841 1.20 Baseline 2023-06-07 16:50:44 -04:00
Sjoerd van de Goor 673b08f174 Merge branch 'master' into printWrongSpawner 2023-06-06 19:19:47 +02:00
Sjoerd van de Goor 989778af26 print in case a wrong spawner is specified 2023-06-06 19:18:47 +02:00
Brian Neumann-Fopiano f5a3a7eb1a wm 2023-06-01 14:11:41 -04:00
Brian Neumann-Fopiano 7a97928c21 added a configurable 2023-05-31 12:19:32 -04:00
1193 changed files with 62111 additions and 15371 deletions
+3
View File
@@ -40,6 +40,9 @@ body:
- 1.17.1
- 1.18
- 1.19
- 1.20
- 1.21
- 1.22
validations:
required: true
- type: input
+1 -1
View File
@@ -10,4 +10,4 @@ libs/
collection/
src/main/java/com/volmit/iris/util/uniques/
/core/src/main/java/com/volmit/iris/util/uniques/
+6 -10
View File
@@ -15,30 +15,27 @@ Consider supporting our development by buying Iris on spigot! We work hard to ma
### Command Line Builds
1. Install [Java JDK 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
1. Install [Java JDK 21](https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html)
2. Set the JDK installation path to `JAVA_HOME` as an environment variable.
* Windows
1. Start > Type `env` and press Enter
2. Advanced > Environment Variables
3. Under System Variables, click `New...`
4. Variable Name: `JAVA_HOME`
5. Variable Value: `C:\Program Files\Java\jdk-17.0.1` (verify this exists after installing java don't just copy
5. Variable Value: `C:\Program Files\Java\jdk-21.0.1` (verify this exists after installing java don't just copy
the example text)
* MacOS
1. Run `/usr/libexec/java_home -V` and look for Java 17
1. Run `/usr/libexec/java_home -V` and look for Java 21
2. Run `sudo nano ~/.zshenv`
3. Add `export JAVA_HOME=$(/usr/libexec/java_home)` as a new line
4. Use `CTRL + X`, then Press `Y`, Then `ENTER`
5. Quit & Reopen Terminal and verify with `echo $JAVA_HOME`. It should print a directory
3. If this is your first time building Iris for MC 1.18+ run `gradlew setup` inside the root Iris project folder.
Otherwise, skip this step. Grab a coffee, this may take up to 5 minutes depending on your cpu & internet connection.
4. Once the project has setup, run `gradlew iris`
5. The Iris jar will be placed in `Iris/build/Iris-XXX-XXX.jar` Enjoy! Consider supporting us by buying it on spigot!
3. Once the project has setup, run `gradlew iris`
4. The Iris jar will be placed in `Iris/build/Iris-XXX-XXX.jar` Enjoy! Consider supporting us by buying it on spigot!
### IDE Builds (for development)
* Run `gradlew setup` any time you get dependency issues with craftbukkit
* Configure ITJ Gradle to use JDK 17 (in settings, search for gradle)
* Configure ITJ Gradle to use JDK 21 (in settings, search for gradle)
* Add a build line in the build.gradle for your own build task to directly compile Iris into your plugins folder if you
prefer.
* Resync the project & run your newly created task (under the development folder in gradle tasks!)
@@ -69,7 +66,6 @@ IrisAccess access=IrisToolbelt.createWorld() // If you like builders...
.name("myWorld") // The world name
.dimension("terrifyinghands")
.seed(69133742) // The world seed
.headless(true) // Headless make gen go fast
.pregen(PregenTask // Define a pregen job to run
.builder()
.center(new Position2(0,0)) // REGION coords (1 region = 32x32 chunks)
-332
View File
@@ -1,332 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2021 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
plugins {
id 'java'
id 'java-library'
id "io.freefair.lombok" version "6.3.0"
id "com.github.johnrengelman.shadow" version "7.1.2"
id "de.undercouch.download" version "5.0.1"
}
version '2.6.2-1.19.4'
def nmsVersion = "1.19.4" //[NMS]
def apiVersion = '1.19'
def specialSourceVersion = '1.11.0' //[NMS]
def spigotJarVersion = '1.19.4-R0.1-SNAPSHOT' //[NMS]
def name = getRootProject().getName() // Defined in settings.gradle
def main = 'com.volmit.iris.Iris'
// ADD YOURSELF AS A NEW LINE IF YOU WANT YOUR OWN BUILD TASK GENERATED
// ======================== WINDOWS =============================
registerCustomOutputTask('Cyberpwn', 'C://Users/cyberpwn/Documents/development/server/plugins')
registerCustomOutputTask('Psycho', 'C://Dan/MinecraftDevelopment/Server/plugins')
registerCustomOutputTask('ArcaneArts', 'C://Users/arcane/Documents/development/server/plugins')
registerCustomOutputTask('Coco', 'D://mcsm/plugins')
registerCustomOutputTask('Strange', 'D://Servers/1.17 Test Server/plugins')
registerCustomOutputTask('Vatuu', 'D://Minecraft/Servers/1.19.4/plugins')
// ========================== UNIX ==============================
registerCustomOutputTaskUnix('CyberpwnLT', '/Users/danielmills/development/server/plugins')
registerCustomOutputTaskUnix('PsychoLT', '/Users/brianfopiano/Desktop/REMOTES/RemoteMinecraft/plugins')
// ==============================================================
/**
* Gradle is weird sometimes, we need to delete the plugin yml from the build folder to actually filter properly.
*/
file(jar.archiveFile.get().getAsFile().getParentFile().getParentFile().getParentFile().getAbsolutePath() + '/build/resources/main/plugin.yml').delete()
/**
* Expand properties into plugin yml
*/
processResources {
filesMatching('**/plugin.yml') {
expand(
'name': name.toString(),
'version': version.toString(),
'main': main.toString(),
'apiversion': apiVersion.toString()
)
}
}
/**
* Unified repo
*/
repositories {
mavenLocal {
content {
includeGroup("org.bukkit")
includeGroup("org.spigotmc")
}
}
mavenCentral()
maven { url "https://arcanearts.jfrog.io/artifactory/archives" }
maven { url "https://mvn.lumine.io/repository/maven-public/" }
maven { url "https://jitpack.io"}
maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots" }
maven { url "https://mvn.lumine.io/repository/maven/" }
maven { url "https://repo.triumphteam.dev/snapshots" }
maven { url "https://repo.mineinabyss.com/releases" }
}
/**
* We need parameter meta for the decree command system
*/
compileJava {
options.compilerArgs << '-parameters'
}
/**
* Configure Iris for shading
*/
shadowJar {
//minimize()
append("plugin.yml")
relocate 'com.dfsek.paralithic', 'com.volmit.iris.util.paralithic'
relocate 'io.papermc.lib', 'com.volmit.iris.util.paper'
relocate 'net.kyori', 'com.volmit.iris.util.kyori'
dependencies {
include(dependency('io.papermc:paperlib'))
include(dependency('com.dfsek:Paralithic'))
include(dependency('net.kyori:'))
}
}
configurations.all {
resolutionStrategy.cacheChangingModulesFor 60, 'minutes'
resolutionStrategy.cacheDynamicVersionsFor 60, 'minutes'
}
/**
* Dependencies.
*
* Provided or classpath dependencies are not shaded and are available on the runtime classpath
*
* Shaded dependencies are not available at runtime, nor are they available on mvn central so they
* need to be shaded into the jar (increasing binary size)
*
* Dynamically loaded dependencies are defined in the plugin.yml (updating these must be updated in the
* plugin.yml also, otherwise they wont be available). These do not increase binary size). Only declare
* these dependencies if they are available on mvn central.
*/
dependencies {
// Provided or Classpath
compileOnly 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'
implementation 'org.spigotmc:spigot-api:1.19.4-R0.1-SNAPSHOT'
implementation 'org.bukkit:craftbukkit:1.19.4-R0.1-SNAPSHOT:remapped-mojang' //[NMS]
// Third Party Integrations
implementation 'com.github.oraxen:oraxen:1.152.5'
implementation 'com.github.LoneDev6:api-itemsadder:3.4.1-r4'
implementation 'me.clip:placeholderapi:2.11.3'
//implementation files('libs/CustomItems.jar')
// Shaded
implementation 'com.dfsek:Paralithic:0.4.0'
implementation 'io.papermc:paperlib:1.0.5'
implementation "net.kyori:adventure-text-minimessage:4.13.1"
implementation 'net.kyori:adventure-platform-bukkit:4.3.0'
implementation 'net.kyori:adventure-api:4.13.1'
implementation 'io.lumine:Mythic-Dist:5.2.1'
// Dynamically Loaded
implementation 'io.timeandspace:smoothie-map:2.0.2'
implementation 'it.unimi.dsi:fastutil:8.5.8'
implementation 'com.googlecode.concurrentlinkedhashmap:concurrentlinkedhashmap-lru:1.4.2'
implementation 'org.zeroturnaround:zt-zip:1.14'
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'org.ow2.asm:asm:9.2'
implementation 'com.google.guava:guava:31.1-jre'
implementation 'bsf:bsf:2.4.0'
implementation 'rhino:js:1.7R2'
implementation 'com.github.ben-manes.caffeine:caffeine:3.0.6'
implementation 'org.apache.commons:commons-lang3:3.12.0'
}
if (JavaVersion.current().toString() != "17") {
System.err.println()
System.err.println("=========================================================================================================")
System.err.println("You must run gradle on Java 17. You are using " + JavaVersion.current())
System.err.println()
System.err.println("=== For IDEs ===")
System.err.println("1. Configure the project for Java 17")
System.err.println("2. Configure the bundled gradle to use Java 17 in settings")
System.err.println()
System.err.println("=== For Command Line (gradlew) ===")
System.err.println("1. Install JDK 17 from https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html")
System.err.println("2. Set JAVA_HOME environment variable to the new jdk installation folder such as C:\\Program Files\\Java\\jdk-17.0.1")
System.err.println("3. Open a new command prompt window to get the new environment variables if need be.")
System.err.println("=========================================================================================================")
System.err.println()
System.exit(69);
}
def buildToolsJar = new File(buildDir, "buildtools/BuildTools.jar");
def specialSourceJar = new File(buildDir, "specialsource/SpecialSource.jar");
def buildToolsFolder = new File(buildDir, "buildtools");
def specialSourceFolder = new File(buildDir, "specialsource");
def buildToolsHint = new File(buildDir, "buildtools/craftbukkit-" + nmsVersion + ".jar");
def outputShadeJar = new File(buildDir, "libs/Iris-" + version + "-all.jar");
def ssiJar = new File(buildDir, "specialsource/Iris-" + version + "-all.jar");
def ssobfJar = new File(buildDir, "specialsource/Iris-" + version + "-rmo.jar");
def ssJar = new File(buildDir, "specialsource/Iris-" + version + "-rma.jar");
def homePath = System.properties['user.home']
def m2 = new File(homePath + "/.m2/repository")
def m2s = m2.getAbsolutePath();
// ======================== Building Mapped Jars =============================
task downloadBuildtools(type: Download) {
group "remapping"
src 'https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar'
dest buildToolsJar
onlyIf {
!buildToolsJar.exists()
}
}
task downloadSpecialSource(type: Download) {
group "remapping"
src 'https://repo.maven.apache.org/maven2/net/md-5/SpecialSource/' + specialSourceVersion + '/SpecialSource-'+specialSourceVersion+'-shaded.jar'
dest specialSourceJar
onlyIf {
!specialSourceJar.exists()
}
}
task executeBuildTools(dependsOn: downloadBuildtools, type: JavaExec)
{
group "remapping"
classpath = files(buildToolsJar)
workingDir = buildToolsFolder
args = [
"--rev",
nmsVersion,
"--compile",
"craftbukkit",
"--remap"
]
onlyIf {
!buildToolsHint.exists()
}
}
task copyBuildToSpecialSource(type: Copy)
{
group "remapping"
from outputShadeJar
into specialSourceFolder
dependsOn(downloadSpecialSource, shadowJar)
}
task specialSourceRemapObfuscate(type: JavaExec)
{
group "remapping"
dependsOn(copyBuildToSpecialSource, downloadSpecialSource, shadowJar)
workingDir = specialSourceFolder
classpath = files(specialSourceJar,
new File(m2s + "/org/spigotmc/spigot/" + spigotJarVersion + "/spigot-" + spigotJarVersion + "-remapped-mojang.jar"))
mainClass = "net.md_5.specialsource.SpecialSource"
args = [
"--live",
"-i",
ssiJar.getName(),
"-o",
ssobfJar.getName(),
"-m",
m2s + "/org/spigotmc/minecraft-server/" + spigotJarVersion + "/minecraft-server-" + spigotJarVersion + "-maps-mojang.txt",
"--reverse",
]
}
task specialSourceRemap(type: JavaExec)
{
group "remapping"
dependsOn(specialSourceRemapObfuscate)
workingDir = specialSourceFolder
classpath = files(specialSourceJar,
new File(m2s + "/org/spigotmc/spigot/" + spigotJarVersion + "/spigot-" + spigotJarVersion + "-remapped-obf.jar"))
mainClass = "net.md_5.specialsource.SpecialSource"
args = [
"--live",
"-i",
ssobfJar.getName(),
"-o",
ssJar.getName(),
"-m",
m2s + "/org/spigotmc/minecraft-server/" + spigotJarVersion + "/minecraft-server-" + spigotJarVersion + "-maps-spigot.csrg"
]
}
tasks.compileJava.dependsOn(executeBuildTools)
compileJava {
options.encoding = "UTF-8"
}
task setup()
{
group("iris")
dependsOn(clean, executeBuildTools)
}
task iris(type: Copy)
{
group "iris"
from ssJar
into buildDir
rename { String fileName ->
fileName.replace('Iris-' + version + '-rma.jar', "Iris-" + version + ".jar")
}
dependsOn(specialSourceRemap)
}
def registerCustomOutputTask(name, path) {
if (!System.properties['os.name'].toLowerCase().contains('windows')) {
return;
}
tasks.register('build' + name, Copy) {
group('development')
outputs.upToDateWhen { false }
dependsOn(iris)
from(new File(buildDir, "Iris-" + version + ".jar"))
into(file(path))
rename { String fileName ->
fileName.replace("Iris-" + version + ".jar", "Iris.jar")
}
}
}
def registerCustomOutputTaskUnix(name, path) {
if (System.properties['os.name'].toLowerCase().contains('windows')) {
return;
}
tasks.register('build' + name, Copy) {
group('development')
outputs.upToDateWhen { false }
dependsOn(iris)
from(new File(buildDir, "Iris-" + version + ".jar"))
into(file(path))
rename { String fileName ->
fileName.replace("Iris-" + version + ".jar", "Iris.jar")
}
}
}
+296
View File
@@ -0,0 +1,296 @@
import de.undercouch.gradle.tasks.download.Download
import org.gradle.jvm.toolchain.JavaLanguageVersion
import xyz.jpenilla.runpaper.task.RunServer
import kotlin.system.exitProcess
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2021 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
buildscript {
repositories.maven("https://jitpack.io")
dependencies.classpath("com.github.VolmitSoftware:NMSTools:c88961416f")
}
plugins {
java
`java-library`
alias(libs.plugins.download)
alias(libs.plugins.runPaper)
}
group = "com.volmit"
version = "3.9.2-1.20.1-26.3"
apply<ApiGenerator>()
// ADD YOURSELF AS A NEW LINE IF YOU WANT YOUR OWN BUILD TASK GENERATED
// ======================== WINDOWS =============================
registerCustomOutputTask("Cyberpwn", "C://Users/cyberpwn/Documents/development/server/plugins")
registerCustomOutputTask("Psycho", "C://Dan/MinecraftDevelopment/Server/plugins")
registerCustomOutputTask("ArcaneArts", "C://Users/arcane/Documents/development/server/plugins")
registerCustomOutputTask("Coco", "D://mcsm/plugins")
registerCustomOutputTask("Strange", "D://Servers/1.17 Test Server/plugins")
registerCustomOutputTask("Vatuu", "D://Minecraft/Servers/1.19.4/plugins")
registerCustomOutputTask("CrazyDev22", "C://Users/Julian/Desktop/server/plugins")
registerCustomOutputTask("PixelFury", "C://Users/repix/workplace/Iris/1.21.3 - Development-Public-v3/plugins")
registerCustomOutputTask("PixelFuryDev", "C://Users/repix/workplace/Iris/1.21 - Development-v3/plugins")
// ========================== UNIX ==============================
registerCustomOutputTaskUnix("CyberpwnLT", "/Users/danielmills/development/server/plugins")
registerCustomOutputTaskUnix("PsychoLT", "/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/plugins")
registerCustomOutputTaskUnix("PixelMac", "/Users/test/Desktop/mcserver/plugins")
registerCustomOutputTaskUnix("CrazyDev22LT", "/home/julian/Desktop/server/plugins")
// ==============================================================
val serverMinHeap = "10G"
val serverMaxHeap = "10G"
val additionalFlags = "-XX:+AlwaysPreTouch"
//Valid values are: none, truecolor, indexed256, indexed16, indexed8
val color = "truecolor"
val errorReporting = "true" == findProperty("errorReporting")
val nmsBindings = mapOf(
"v26_3_R1" to "26.3.build.176-stable",
"v26_1_R1" to "26.1.2.build.74-stable",
"v1_21_R7" to "1.21.11-R0.1-SNAPSHOT",
"v1_21_R6" to "1.21.10-R0.1-SNAPSHOT",
"v1_21_R5" to "1.21.8-R0.1-SNAPSHOT",
"v1_21_R4" to "1.21.5-R0.1-SNAPSHOT",
"v1_21_R3" to "1.21.4-R0.1-SNAPSHOT",
"v1_21_R2" to "1.21.3-R0.1-SNAPSHOT",
"v1_21_R1" to "1.21.1-R0.1-SNAPSHOT",
"v1_20_R4" to "1.20.6-R0.1-SNAPSHOT",
"v1_20_R3" to "1.20.4-R0.1-SNAPSHOT",
"v1_20_R2" to "1.20.2-R0.1-SNAPSHOT",
"v1_20_R1" to "1.20.1-R0.1-SNAPSHOT",
)
val jvmVersion = mapOf("v26_3_R1" to 25, "v26_1_R1" to 25)
// From 26.1 on the server is not obfuscated, so these bindings compile against Paper's dev bundle as-is.
val userDevBindings = setOf("v26_3_R1", "v26_1_R1")
nmsBindings.forEach { (key, value) ->
project(":nms:$key") {
apply<JavaPlugin>()
nmsBinding {
jvm = jvmVersion.getOrDefault(key, 21)
version = value
type = if (key in userDevBindings) NMSBinding.Type.USER_DEV else NMSBinding.Type.DIRECT
}
dependencies {
compileOnly(project(":core"))
compileOnly(rootProject.libs.annotations)
compileOnly(rootProject.libs.byteBuddy.core)
}
}
tasks.register<RunServer>("runServer-$key") {
group = "servers"
minecraftVersion(value.substringBefore("-").substringBefore(".build"))
minHeapSize = serverMinHeap
maxHeapSize = serverMaxHeap
pluginJars(tasks.jar.flatMap { it.archiveFile })
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(jvmVersion.getOrDefault(key, 21))}
runDirectory.convention(layout.buildDirectory.dir("run/$key"))
systemProperty("disable.watchdog", "true")
systemProperty("net.kyori.ansi.colorLevel", color)
systemProperty("com.mojang.eula.agree", true)
systemProperty("iris.suppressReporting", !errorReporting)
jvmArgs("-javaagent:${project(":core:agent").tasks.jar.flatMap { it.archiveFile }.get().asFile.absolutePath}")
jvmArgs(additionalFlags.split(' '))
}
}
val included: Configuration by configurations.creating
val jarJar: Configuration by configurations.creating
dependencies {
for (key in nmsBindings.keys) {
included(project(":nms:$key", if (key in userDevBindings) "runtimeElements" else "reobf"))
}
included(project(":core", "shadow"))
jarJar(project(":core:agent"))
}
tasks {
jar {
inputs.files(included)
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from(jarJar, provider { included.resolve().map(::zipTree) })
archiveFileName.set("Iris-${project.version}.jar")
}
register<Copy>("iris") {
group = "iris"
dependsOn("jar")
from(layout.buildDirectory.file("libs/Iris-${project.version}.jar"))
into(layout.buildDirectory)
}
register<Copy>("irisDev") {
group = "iris"
from(project(":core").layout.buildDirectory.files("libs/core-javadoc.jar", "libs/core-sources.jar"))
rename { it.replace("core", "Iris-${project.version}") }
into(layout.buildDirectory)
dependsOn(":core:sourcesJar")
dependsOn(":core:javadocJar")
}
val cli = file("sentry-cli.exe")
register<Download>("downloadCli") {
group = "io.sentry"
src("https://release-registry.services.sentry.io/apps/sentry-cli/latest?response=download&arch=x86_64&platform=${System.getProperty("os.name")}&package=sentry-cli")
dest(cli)
doLast {
cli.setExecutable(true)
}
}
register("release") {
group = "io.sentry"
dependsOn("downloadCli")
doLast {
val url = "http://sentry.volmit.com:8080"
val authToken = project.findProperty("sentry.auth.token") ?: System.getenv("SENTRY_AUTH_TOKEN")
val org = "sentry"
val projectName = "iris"
exec(cli, "--url", url , "--auth-token", authToken, "releases", "new", "-o", org, "-p", projectName, version)
exec(cli, "--url", url , "--auth-token", authToken, "releases", "set-commits", "-o", org, "-p", projectName, version, "--auto", "--ignore-missing")
//exec(cli, "--url", url, "--auth-token", authToken, "releases", "finalize", "-o", org, "-p", projectName, version)
cli.delete()
}
}
}
fun exec(vararg command: Any) {
val p = ProcessBuilder(command.map { it.toString() })
.start()
p.inputStream.reader().useLines { it.forEach(::println) }
p.errorStream.reader().useLines { it.forEach(::println) }
p.waitFor()
}
configurations.configureEach {
resolutionStrategy.cacheChangingModulesFor(60, "minutes")
resolutionStrategy.cacheDynamicVersionsFor(60, "minutes")
}
allprojects {
apply<JavaPlugin>()
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(jvmVersion.getOrDefault(name, 21)))
}
}
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
maven("https://repo.codemc.org/repository/maven-public/")
maven("https://jitpack.io") // EcoItems, score
maven("https://repo.nexomc.com/releases/") // nexo
maven("https://maven.devs.beer/") // itemsadder
maven("https://repo.extendedclip.com/releases/") // placeholderapi
maven("https://mvn.lumine.io/repository/maven-public/") // mythic
maven("https://nexus.phoenixdevt.fr/repository/maven-public/") //MMOItems
maven("https://repo.onarandombox.com/content/groups/public/") //Multiverse Core
maven("https://repo.momirealms.net/releases/") //CraftEngine
}
dependencies {
// Provided or Classpath
compileOnly(rootProject.libs.lombok)
annotationProcessor(rootProject.libs.lombok)
}
/**
* We need parameter meta for the decree command system
*/
tasks {
compileJava {
options.compilerArgs.add("-parameters")
options.encoding = "UTF-8"
}
javadoc {
options.encoding = "UTF-8"
options.quiet()
//options.addStringOption("Xdoclint:none") // TODO: Re-enable this
}
register<Jar>("sourcesJar") {
archiveClassifier.set("sources")
from(sourceSets.main.map { it.allSource })
}
register<Jar>("javadocJar") {
archiveClassifier.set("javadoc")
from(javadoc.map { it.destinationDir!! })
}
}
}
if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_21)) {
System.err.println()
System.err.println("=========================================================================================================")
System.err.println("You must run gradle on Java 21 or newer. You are using " + JavaVersion.current())
System.err.println()
System.err.println("=== For IDEs ===")
System.err.println("1. Configure the project for Java 21 toolchain")
System.err.println("2. Configure the bundled gradle to use Java 21+ in settings")
System.err.println()
System.err.println("=== For Command Line (gradlew) ===")
System.err.println("1. Install JDK 21 from https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html")
System.err.println("2. Set JAVA_HOME environment variable to the new jdk installation folder such as C:\\Program Files\\Java\\jdk-21.0.4")
System.err.println("3. Open a new command prompt window to get the new environment variables if need be.")
System.err.println("=========================================================================================================")
System.err.println()
exitProcess(69)
}
fun registerCustomOutputTask(name: String, path: String) {
if (!System.getProperty("os.name").lowercase().contains("windows")) {
return
}
tasks.register<Copy>("build$name") {
group = "development"
outputs.upToDateWhen { false }
dependsOn("iris")
from(layout.buildDirectory.file("Iris-${project.version}.jar"))
into(file(path))
rename { "Iris.jar" }
}
}
fun registerCustomOutputTaskUnix(name: String, path: String) {
if (System.getProperty("os.name").lowercase().contains("windows")) {
return
}
tasks.register<Copy>("build$name") {
group = "development"
outputs.upToDateWhen { false }
dependsOn("iris")
from(layout.buildDirectory.file("Iris-${project.version}.jar"))
into(file(path))
rename { "Iris.jar" }
}
}
+32
View File
@@ -0,0 +1,32 @@
import org.gradle.jvm.toolchain.JavaLanguageVersion
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
kotlin("jvm") version embeddedKotlinVersion
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
kotlin {
jvmToolchain(21)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
}
}
repositories {
mavenCentral()
gradlePluginPortal()
maven("https://jitpack.io")
}
dependencies {
implementation("org.ow2.asm:asm:9.8")
implementation("com.github.VolmitSoftware:NMSTools:c88961416f")
implementation("io.papermc.paperweight:paperweight-userdev:2.0.0-beta.21")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
}
+121
View File
@@ -0,0 +1,121 @@
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.jvm.tasks.Jar
import org.objectweb.asm.*
import java.io.File
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
class ApiGenerator : Plugin<Project> {
override fun apply(target: Project): Unit = with(target) {
plugins.apply("maven-publish")
val task = tasks.register("irisApi", GenerateApiTask::class.java)
extensions.findByType(PublishingExtension::class.java)!!.apply {
repositories.maven {
it.name = "deployDir"
it.url = targetDirectory.toURI()
}
publications.create("maven", MavenPublication::class.java) {
it.groupId = name
it.version = version.toString()
it.artifact(task)
}
}
}
}
abstract class GenerateApiTask : DefaultTask() {
init {
group = "iris"
dependsOn("jar")
finalizedBy("publishMavenPublicationToDeployDirRepository")
doLast {
logger.lifecycle("The API is located at ${outputFile.absolutePath}")
}
}
@InputFile
val inputFile: File = project.tasks
.named("jar", Jar::class.java)
.get()
.archiveFile
.get()
.asFile
@OutputFile
val outputFile: File = project.targetDirectory.resolve(inputFile.name)
@TaskAction
fun generate() {
JarFile(inputFile).use { jar ->
JarOutputStream(outputFile.apply { parentFile?.mkdirs() }.outputStream()).use { out ->
jar.stream()
.parallel()
.filter { !it.isDirectory }
.filter { it.name.endsWith(".class") }
.forEach {
val bytes = jar.getInputStream(it).use { input ->
val writer = ClassWriter(ClassWriter.COMPUTE_MAXS)
val visitor = MethodClearingVisitor(writer)
ClassReader(input).accept(visitor, 0)
writer.toByteArray()
}
synchronized(out) {
out.putNextEntry(it)
out.write(bytes)
out.closeEntry()
}
}
}
}
}
}
val Project.targetDirectory: File get() {
val dir = System.getenv("DEPLOY_DIR") ?: return project.layout.buildDirectory.dir("api").get().asFile
return File(dir)
}
private class MethodClearingVisitor(
cv: ClassVisitor
) : ClassVisitor(Opcodes.ASM9, cv) {
override fun visitMethod(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
exceptions: Array<out String>?
) = ExceptionThrowingMethodVisitor(super.visitMethod(access, name, descriptor, signature, exceptions))
}
private class ExceptionThrowingMethodVisitor(
mv: MethodVisitor
) : MethodVisitor(Opcodes.ASM9, mv) {
override fun visitCode() {
if (mv == null) return
mv.visitCode()
mv.visitTypeInsn(Opcodes.NEW, "java/lang/IllegalStateException")
mv.visitInsn(Opcodes.DUP)
mv.visitLdcInsn("Only API")
mv.visitMethodInsn(
Opcodes.INVOKESPECIAL,
"java/lang/IllegalStateException",
"<init>", "(Ljava/lang/String;)V", false
)
mv.visitInsn(Opcodes.ATHROW)
mv.visitMaxs(0, 0)
mv.visitEnd()
}
}
+187
View File
@@ -0,0 +1,187 @@
import NMSBinding.Type
import com.volmit.nmstools.NMSToolsExtension
import com.volmit.nmstools.NMSToolsPlugin
import io.papermc.paperweight.userdev.PaperweightUser
import io.papermc.paperweight.userdev.PaperweightUserDependenciesExtension
import io.papermc.paperweight.userdev.PaperweightUserExtension
import io.papermc.paperweight.userdev.attribute.Obfuscation
import io.papermc.paperweight.util.constants.REOBF_CONFIG
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.gradle.api.*
import org.gradle.api.attributes.Bundling
import org.gradle.api.attributes.Category
import org.gradle.api.attributes.LibraryElements
import org.gradle.api.attributes.Usage
import org.gradle.api.model.ObjectFactory
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.TaskAction
import org.gradle.internal.extensions.core.extra
import org.gradle.jvm.toolchain.JavaLanguageVersion
import org.gradle.jvm.toolchain.JavaToolchainService
import org.gradle.work.DisableCachingByDefault
import java.io.RandomAccessFile
import javax.inject.Inject
class NMSBinding : Plugin<Project> {
override fun apply(target: Project): Unit = with(target) {
val config = extra["nms"] as? Config ?: throw GradleException("No NMS binding configuration found")
val jvm = config.jvm
val type = config.type
if (type == Type.USER_DEV) {
plugins.apply(PaperweightUser::class.java)
dependencies.extensions.findByType(PaperweightUserDependenciesExtension::class.java)
?.paperDevBundle(config.version)
val java = extensions.findByType(JavaPluginExtension::class.java) ?: throw GradleException("Java plugin not found")
java.toolchain.languageVersion.set(JavaLanguageVersion.of(jvm))
val javaToolchains = project.extensions.getByType(JavaToolchainService::class.java) ?: throw GradleException("Java toolchain service not found")
extensions.configure(PaperweightUserExtension::class.java) {
it.javaLauncher.set(javaToolchains.launcherFor(java.toolchain))
}
} else {
extra["nmsTools.useBuildTools"] = type == Type.BUILD_TOOLS
plugins.apply(NMSToolsPlugin::class.java)
extensions.configure(NMSToolsExtension::class.java) {
it.jvm.set(jvm)
it.version.set(config.version)
}
registerReobfConfiguration("remap")
}
val (major, minor) = config.version.parseVersion()
if (major <= 20 && minor <= 4) return@with
tasks.register("convert", ConversionTask::class.java, type)
tasks.named("compileJava") { it.dependsOn("convert") }
rootProject.tasks.named("prepareKotlinBuildScriptModel") { it.dependsOn("$path:convert") }
}
private fun Project.registerReobfConfiguration(artifactTask: String) {
val conf = configurations.create(REOBF_CONFIG)
conf.isCanBeConsumed = true
conf.isCanBeResolved = false
conf.attributes {
it.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
it.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
it.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR))
it.attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
it.attribute(Obfuscation.OBFUSCATION_ATTRIBUTE, objects.named(Obfuscation.OBFUSCATED))
}
conf.outgoing.artifact(tasks.named(artifactTask))
}
@DisableCachingByDefault
abstract class ConversionTask @Inject constructor(type: Type) : DefaultTask() {
private val pattern: Regex
private val replacement: String
init {
group = "nms"
inputs.property("type", type)
val java = project.extensions.findByType(JavaPluginExtension::class.java) ?: throw GradleException("Java plugin not found")
val source = java.sourceSets.named("main").map { it.allJava }
inputs.files(source)
outputs.files(source)
if (type == Type.USER_DEV) {
pattern = "org\\.bukkit\\.craftbukkit\\.${project.name}".toRegex()
replacement = "org.bukkit.craftbukkit"
} else {
pattern = "org\\.bukkit\\.craftbukkit\\.(?!${project.name})".toRegex()
replacement = "org.bukkit.craftbukkit.${project.name}."
}
}
@TaskAction
fun process() {
val dispatcher = Dispatchers.IO.limitedParallelism(16)
runBlocking {
for (file in inputs.files) {
if (file.extension !in listOf("java"))
continue
launch(dispatcher) {
val output = ArrayList<String>()
var changed = false
file.bufferedReader().use {
for (line in it.lines()) {
if (line.startsWith("package") || line.isBlank()) {
output += line
continue
}
if (!line.startsWith("import")) {
if (!changed) return@launch
else {
output += line
continue
}
}
if (!line.contains(pattern)) {
output += line
continue
}
output += line.replace(pattern, replacement)
changed = true
}
}
if (changed) {
RandomAccessFile(file, "r").use { raf ->
val bytes = ByteArray(NEW_LINE_BYTES.size)
raf.seek(raf.length() - bytes.size)
raf.readFully(bytes)
if (bytes.contentEquals(NEW_LINE_BYTES))
output += ""
}
file.writer().use {
val iterator = output.iterator()
while (iterator.hasNext()) {
it.append(iterator.next())
if (iterator.hasNext())
it.append(NEW_LINE)
}
}
}
}
}
}
}
}
enum class Type {
USER_DEV,
BUILD_TOOLS,
DIRECT,
}
}
private val NEW_LINE = System.lineSeparator()
private val NEW_LINE_BYTES = NEW_LINE.encodeToByteArray()
// "1.21.11-R0.1-SNAPSHOT" -> (21, 11), "26.1.2.build.74-stable" -> (26, 1)
private fun String.parseVersion() = substringBefore('-').split(".").let {
if (it[0] == "1") it[1].toInt() to it[2].toInt()
else it[0].toInt() to it[1].toInt()
}
class Config(
var jvm: Int = 21,
var type: Type = Type.DIRECT
) {
lateinit var version: String
}
fun Project.nmsBinding(action: Config.() -> Unit) {
extra["nms"] = Config().apply(action)
plugins.apply(NMSBinding::class.java)
}
private inline fun <reified T : Named> ObjectFactory.named(name: String): T = named(T::class.java, name)
+12
View File
@@ -0,0 +1,12 @@
plugins {
java
}
tasks.jar {
manifest.attributes(
"Agent-Class" to "com.volmit.iris.util.agent.Installer",
"Premain-Class" to "com.volmit.iris.util.agent.Installer",
"Can-Redefine-Classes" to true,
"Can-Retransform-Classes" to true
)
}
@@ -0,0 +1,29 @@
package com.volmit.iris.util.agent;
import java.lang.instrument.Instrumentation;
public class Installer {
private static volatile Instrumentation instrumentation;
public static Instrumentation getInstrumentation() {
Instrumentation instrumentation = Installer.instrumentation;
if (instrumentation == null) {
throw new IllegalStateException("The agent is not loaded or this method is not called via the system class loader");
}
return instrumentation;
}
public static void premain(String arguments, Instrumentation instrumentation) {
doMain(instrumentation);
}
public static void agentmain(String arguments, Instrumentation instrumentation) {
doMain(instrumentation);
}
private static synchronized void doMain(Instrumentation instrumentation) {
if (Installer.instrumentation != null)
return;
Installer.instrumentation = instrumentation;
}
}
+231
View File
@@ -0,0 +1,231 @@
import io.github.slimjar.func.slimjarHelper
import io.github.slimjar.resolver.data.Mirror
import org.ajoberstar.grgit.Grgit
import org.gradle.jvm.toolchain.JavaLanguageVersion
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.net.URI
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2021 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
plugins {
java
`java-library`
alias(libs.plugins.shadow)
alias(libs.plugins.sentry)
alias(libs.plugins.slimjar)
alias(libs.plugins.grgit)
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.lombok)
}
val apiVersion = "1.19"
val main = "com.volmit.iris.Iris"
val lib = "com.volmit.iris.util"
/**
* Dependencies.
*
* Provided or classpath dependencies are not shaded and are available on the runtime classpath
*
* Shaded dependencies are not available at runtime, nor are they available on mvn central so they
* need to be shaded into the jar (increasing binary size)
*
* Dynamically loaded dependencies are defined in the plugin.yml (updating these must be updated in the
* plugin.yml also, otherwise they wont be available). These do not increase binary size). Only declare
* these dependencies if they are available on mvn central.
*/
dependencies {
// Provided or Classpath
compileOnly(libs.paper)
compileOnly(platform(libs.adventure.bom))
compileOnly(libs.log4j.api)
compileOnly(libs.log4j.core)
// Third Party Integrations
compileOnly(libs.nexo)
compileOnly(libs.itemsadder)
compileOnly(libs.placeholderApi)
compileOnly(libs.score)
compileOnly(libs.mmoitems)
compileOnly(libs.ecoitems)
compileOnly(libs.mythic)
compileOnly(libs.mythicChrucible)
compileOnly(libs.kgenerators) {
isTransitive = false
}
compileOnly(libs.multiverseCore)
compileOnly(libs.craftengine.core)
compileOnly(libs.craftengine.bukkit)
//compileOnly(libs.sparrowNbt)
// Shaded
implementation(slimjarHelper("spigot"))
// Dynamically Loaded
slim(libs.paralithic)
slim(libs.bstats)
slim(libs.sentry)
slim(libs.commons.io)
slim(libs.commons.lang)
slim(libs.commons.lang3)
slim(libs.commons.math3)
slim(libs.oshi)
slim(libs.lz4)
slim(libs.fastutil)
slim(libs.lru)
slim(libs.zip)
slim(libs.gson)
slim(libs.asm)
slim(libs.caffeine)
slim(libs.byteBuddy.core)
slim(libs.byteBuddy.agent)
slim(libs.dom4j)
slim(libs.jaxen)
slim(libs.flatlaf)
slim(libs.flatlaf.extras)
// Script Engine
slim(libs.kotlin.stdlib)
slim(libs.kotlin.coroutines)
slim(libs.kotlin.scripting.common)
slim(libs.kotlin.scripting.jvm)
slim(libs.kotlin.scripting.jvm.host)
slim(libs.kotlin.scripting.dependencies.maven) {
constraints {
slim(libs.mavenCore)
}
}
}
java {
disableAutoTargetJvm()
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
kotlin {
jvmToolchain(21)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
}
}
sentry {
url = "http://sentry.volmit.com:8080"
autoInstallation.enabled = false
includeSourceContext = true
org = "sentry"
projectName = "iris"
authToken = findProperty("sentry.auth.token") as String? ?: System.getenv("SENTRY_AUTH_TOKEN")
}
slimJar {
mirrors = listOf(Mirror(
URI.create("https://maven-central.storage-download.googleapis.com/maven2").toURL(),
URI.create("https://repo.maven.apache.org/maven2/").toURL()
))
relocate("com.dfsek.paralithic", "$lib.paralithic")
relocate("org.bstats", "$lib.metrics")
relocate("io.sentry", "$lib.sentry")
relocate("org.apache.maven", "$lib.maven")
relocate("org.codehaus.plexus", "$lib.plexus")
relocate("org.eclipse.sisu", "$lib.sisu")
relocate("org.eclipse.aether", "$lib.aether")
relocate("com.google.inject", "$lib.guice")
relocate("org.dom4j", "$lib.dom4j")
relocate("org.jaxen", "$lib.jaxen")
relocate("com.github.benmanes.caffeine", "$lib.caffeine")
}
tasks {
/**
* We need parameter meta for the decree command system
*/
compileJava {
options.compilerArgs.add("-parameters")
options.encoding = "UTF-8"
}
/**
* Expand properties into plugin yml
*/
processResources {
inputs.properties(
"name" to rootProject.name,
"version" to rootProject.version,
"apiVersion" to apiVersion,
"main" to main,
)
filesMatching("**/plugin.yml") {
expand(inputs.properties)
}
}
shadowJar {
mergeServiceFiles()
//minimize()
relocate("io.github.slimjar", "$lib.slimjar")
exclude("modules/loader-agent.isolated-jar")
}
sentryCollectSourcesJava {
dependsOn(generateTemplates)
}
}
val templateSource = file("src/main/templates")
val templateDest = layout.buildDirectory.dir("generated/sources/templates")!!
val generateTemplates = tasks.register<Copy>("generateTemplates") {
inputs.properties(
"environment" to when {
project.hasProperty("release") -> "production"
project.hasProperty("argghh") -> "Argghh!"
else -> "development"
},
"commit" to provider {
val res = runCatching { project.extensions.getByType<Grgit>().head().id }
res.getOrDefault("")
.takeIf { it.length == 40 } ?: run {
this.logger.error("Git commit hash not found", res.exceptionOrNull())
"unknown"
}
},
)
from(templateSource)
into(templateDest)
rename { "com/volmit/iris/$it" }
expand(inputs.properties)
}
tasks.generateSentryBundleIdJava {
dependsOn(generateTemplates)
}
rootProject.tasks.named("prepareKotlinBuildScriptModel") {
dependsOn(generateTemplates)
}
sourceSets.main {
java.srcDir(generateTemplates.map { it.outputs })
}
@@ -23,8 +23,11 @@ import com.volmit.iris.Iris;
import com.volmit.iris.util.io.IO;
import com.volmit.iris.util.json.JSONException;
import com.volmit.iris.util.json.JSONObject;
import com.volmit.iris.util.misc.getHardware;
import com.volmit.iris.util.plugin.VolmitSender;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.File;
import java.io.IOException;
@@ -41,13 +44,15 @@ public class IrisSettings {
private IrisSettingsConcurrency concurrency = new IrisSettingsConcurrency();
private IrisSettingsStudio studio = new IrisSettingsStudio();
private IrisSettingsPerformance performance = new IrisSettingsPerformance();
private IrisSettingsUpdater updater = new IrisSettingsUpdater();
private IrisSettingsPregen pregen = new IrisSettingsPregen();
private IrisSettingsSentry sentry = new IrisSettingsSentry();
public static int getThreadCount(int c) {
return switch (c) {
return Math.max(switch (c) {
case -1, -2, -4 -> Runtime.getRuntime().availableProcessors() / -c;
case 0, 1, 2 -> 1;
default -> Math.max(c, 2);
};
}, 1);
}
public static IrisSettings get() {
@@ -126,36 +131,122 @@ public class IrisSettings {
public boolean markerEntitySpawningSystem = true;
public boolean effectSystem = true;
public boolean worldEditWandCUI = true;
public boolean globalPregenCache = false;
}
@Data
public static class IrisSettingsConcurrency {
public int parallelism = -1;
public int ioParallelism = -2;
public int worldGenParallelism = -1;
public int getWorldGenThreads() {
return getThreadCount(worldGenParallelism);
}
}
@Data
public static class IrisSettingsPregen {
private static final int MIN_RESIDENT_TECTONIC_PLATES = 16;
private static final double MANTLE_HEAP_FRACTION = 0.6D;
private static final int MEASURED_PLATE_MEGABYTES = 38;
private static final int MEASURED_WORLD_HEIGHT = 1088;
public boolean useCacheByDefault = true;
public boolean useHighPriority = false;
public boolean useVirtualThreads = false;
public boolean useTicketQueue = true;
public int maxConcurrency = 256;
public int maxResidentTectonicPlates = 96;
public int mantleBackpressureWaitMs = 25;
public int mantleBackpressureTimeoutMs = 60_000;
public int getEffectiveResidentTectonicPlates(int worldHeight) {
// Mantle chunks only hold the sections something was written to, so shorter worlds keep the measured size.
double heightScale = Math.max(1D, (double) worldHeight / MEASURED_WORLD_HEIGHT);
double plateMegabytes = MEASURED_PLATE_MEGABYTES * heightScale;
int heapBudget = (int) Math.floor(MANTLE_HEAP_FRACTION * getHardware.getProcessMemory() / plateMegabytes);
return Math.max(MIN_RESIDENT_TECTONIC_PLATES, Math.min(maxResidentTectonicPlates, heapBudget));
}
public int getMantleBackpressureWaitMs() {
return Math.max(5, Math.min(mantleBackpressureWaitMs, 1_000));
}
public int getMantleBackpressureTimeoutMs() {
return Math.max(5_000, Math.min(mantleBackpressureTimeoutMs, 600_000));
}
}
@Data
public static class IrisSettingsPerformance {
public boolean trimMantleInStudio = false;
private IrisSettingsEngineSVC engineSVC = new IrisSettingsEngineSVC();
public boolean trimMantleInStudio = false;
public int mantleKeepAlive = 30;
public int cacheSize = 4_096;
public int noiseCacheSize = 1_024;
public int resourceLoaderCacheSize = 1_024;
public int objectLoaderCacheSize = 4_096;
public int scriptLoaderCacheSize = 512;
public int tectonicPlateSize = -1;
public int mantleCleanupDelay = 200;
public int getTectonicPlateSize() {
if (tectonicPlateSize > 0)
return tectonicPlateSize;
return (int) (getHardware.getProcessMemory() / 512L);
}
}
@Data
public static class IrisSettingsUpdater {
public int maxConcurrency = 256;
public boolean nativeThreads = false;
public double threadMultiplier = 2;
public double chunkLoadSensitivity = 0.7;
public MsRange emptyMsRange = new MsRange(80, 100);
public MsRange defaultMsRange = new MsRange(20, 40);
public int getMaxConcurrency() {
return Math.max(Math.abs(maxConcurrency), 1);
}
public double getThreadMultiplier() {
return Math.min(Math.abs(threadMultiplier), 0.1);
}
public double getChunkLoadSensitivity() {
return Math.min(chunkLoadSensitivity, 0.9);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class MsRange {
public int min = 20;
public int max = 40;
}
@Data
public static class IrisSettingsGeneral {
public boolean DoomsdayAnnihilationSelfDestructMode = false;
public boolean commandSounds = true;
public boolean debug = false;
public boolean dumpMantleOnError = false;
public boolean disableNMS = false;
public boolean pluginMetrics = true;
public boolean splashLogoStartup = true;
public boolean useConsoleCustomColors = true;
public boolean useCustomColorsIngame = true;
public boolean adjustVanillaHeight = false;
public String forceMainWorld = "";
public int spinh = -20;
public int spins = 7;
public int spinb = 8;
public String cartographerMessage = "Iris does not allow cartographers in its world due to crashes.";
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean canUseCustomColors(VolmitSender volmitSender) {
@@ -163,10 +254,19 @@ public class IrisSettings {
}
}
@Data
public static class IrisSettingsSentry {
public boolean includeServerId = true;
public boolean disableAutoReporting = false;
public boolean debug = false;
}
@Data
public static class IrisSettingsGUI {
public boolean useServerLaunchedGuis = true;
public boolean maximumPregenGuiFPS = false;
public boolean colorMode = true;
public String theme = "auto";
}
@Data
@@ -174,6 +274,10 @@ public class IrisSettings {
public String defaultWorldType = "overworld";
public int maxBiomeChildDepth = 4;
public boolean preventLeafDecay = true;
public boolean useMulticore = false;
public boolean useMulticoreMantle = false;
public boolean offsetNoiseTypes = false;
public boolean earlyCustomBlocks = false;
}
@Data
@@ -183,4 +287,15 @@ public class IrisSettings {
public boolean disableTimeAndWeather = true;
public boolean autoStartDefaultStudio = false;
}
@Data
public static class IrisSettingsEngineSVC {
public boolean useVirtualThreads = true;
public boolean forceMulticoreWrite = false;
public int priority = Thread.NORM_PRIORITY;
public int getPriority() {
return Math.max(Math.min(priority, Thread.MAX_PRIORITY), Thread.MIN_PRIORITY);
}
}
}
@@ -0,0 +1,137 @@
package com.volmit.iris.core;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.volmit.iris.Iris;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.core.world.WorldStorage;
import com.volmit.iris.engine.data.cache.AtomicCache;
import com.volmit.iris.engine.object.IrisDimension;
import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.io.IO;
import com.volmit.iris.util.misc.ServerProperties;
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
public class IrisWorlds {
private static final AtomicCache<IrisWorlds> cache = new AtomicCache<>();
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final Type TYPE = TypeToken.getParameterized(KMap.class, String.class, String.class).getType();
private final KMap<String, String> worlds;
private volatile boolean dirty = false;
private IrisWorlds(KMap<String, String> worlds) {
this.worlds = worlds;
readBukkitWorlds().forEach(this::put0);
save();
}
public static IrisWorlds get() {
return cache.aquire(() -> new IrisWorlds(readWorldsFile()));
}
public static Set<String> readKnownWorldNames() {
Set<String> names = new HashSet<>(readWorldsFile().keySet());
names.addAll(readBukkitWorlds().keySet());
return names;
}
private static KMap<String, String> readWorldsFile() {
File file = Iris.instance.getDataFile("worlds.json");
if (!file.exists()) {
return new KMap<>();
}
try {
String json = IO.readAll(file);
KMap<String, String> worlds = GSON.fromJson(json, TYPE);
return Objects.requireNonNullElseGet(worlds, KMap::new);
} catch (Throwable e) {
Iris.error("Failed to load worlds.json!");
e.printStackTrace();
Iris.reportError(e);
}
return new KMap<>();
}
public void put(String name, String type) {
put0(name, type);
save();
}
private void put0(String name, String type) {
String old = worlds.put(name, type);
if (!type.equals(old))
dirty = true;
}
public KMap<String, String> getWorlds() {
clean();
return readBukkitWorlds().put(worlds);
}
public Stream<IrisData> getPacks() {
return getDimensions()
.map(IrisDimension::getLoader)
.filter(Objects::nonNull);
}
public Stream<IrisDimension> getDimensions() {
return getWorlds()
.entrySet()
.stream()
.map(entry -> Iris.loadDimension(entry.getKey(), entry.getValue()))
.filter(Objects::nonNull);
}
public void clean() {
dirty = worlds.entrySet().removeIf(entry -> !new File(WorldStorage.get().worldFolder(entry.getKey()), "iris/pack/dimensions/" + entry.getValue() + ".json").exists());
}
public synchronized void save() {
clean();
if (!dirty) return;
try {
IO.write(Iris.instance.getDataFile("worlds.json"), OutputStreamWriter::new, writer -> GSON.toJson(worlds, TYPE, writer));
dirty = false;
} catch (IOException e) {
Iris.error("Failed to save worlds.json!");
e.printStackTrace();
Iris.reportError(e);
}
}
public static KMap<String, String> readBukkitWorlds() {
var bukkit = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
var worlds = bukkit.getConfigurationSection("worlds");
if (worlds == null) return new KMap<>();
var result = new KMap<String, String>();
for (String world : worlds.getKeys(false)) {
var gen = worlds.getString(world + ".generator");
if (gen == null) continue;
String loadKey;
if (gen.equalsIgnoreCase("iris")) {
loadKey = IrisSettings.get().getGenerator().getDefaultWorldType();
} else if (gen.startsWith("Iris:")) {
loadKey = gen.substring(5);
} else continue;
result.put(world, loadKey);
}
return result;
}
}
@@ -0,0 +1,311 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core;
import com.volmit.iris.Iris;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.core.nms.INMS;
import com.volmit.iris.core.nms.datapack.DataVersion;
import com.volmit.iris.core.nms.datapack.IDataFixer;
import com.volmit.iris.core.world.WorldStorage;
import com.volmit.iris.engine.object.*;
import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.collection.KSet;
import com.volmit.iris.util.format.C;
import com.volmit.iris.util.misc.ServerProperties;
import com.volmit.iris.util.plugin.VolmitSender;
import com.volmit.iris.util.scheduling.J;
import lombok.NonNull;
import lombok.SneakyThrows;
import org.bukkit.Bukkit;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.stream.Stream;
public class ServerConfigurator {
public static void configure() {
IrisSettings.IrisSettingsAutoconfiguration s = IrisSettings.get().getAutoConfiguration();
if (s.isConfigureSpigotTimeoutTime()) {
J.attempt(ServerConfigurator::increaseKeepAliveSpigot);
}
if (s.isConfigurePaperWatchdogDelay()) {
J.attempt(ServerConfigurator::increasePaperWatchdog);
}
installDataPacks(true);
}
private static void increaseKeepAliveSpigot() throws IOException, InvalidConfigurationException {
File spigotConfig = new File("spigot.yml");
FileConfiguration f = new YamlConfiguration();
f.load(spigotConfig);
long tt = f.getLong("settings.timeout-time");
long spigotTimeout = TimeUnit.MINUTES.toSeconds(5);
if (tt < spigotTimeout) {
Iris.warn("Updating spigot.yml timeout-time: " + tt + " -> " + spigotTimeout + " (5 minutes)");
Iris.warn("You can disable this change (autoconfigureServer) in Iris settings, then change back the value.");
f.set("settings.timeout-time", spigotTimeout);
f.save(spigotConfig);
}
}
private static void increasePaperWatchdog() throws IOException, InvalidConfigurationException {
File spigotConfig = new File("config/paper-global.yml");
FileConfiguration f = new YamlConfiguration();
f.load(spigotConfig);
long tt = f.getLong("watchdog.early-warning-delay");
long watchdog = TimeUnit.MINUTES.toMillis(3);
if (tt < watchdog) {
Iris.warn("Updating paper.yml watchdog early-warning-delay: " + tt + " -> " + watchdog + " (3 minutes)");
Iris.warn("You can disable this change (autoconfigureServer) in Iris settings, then change back the value.");
f.set("watchdog.early-warning-delay", watchdog);
f.save(spigotConfig);
}
}
private static KList<File> getDatapacksFolder() {
if (!IrisSettings.get().getGeneral().forceMainWorld.isEmpty()) {
return new KList<File>().qadd(new File(Bukkit.getWorldContainer(), IrisSettings.get().getGeneral().forceMainWorld + "/datapacks"));
}
return WorldStorage.get().datapackFolders();
}
public static boolean installDataPacks(boolean fullInstall) {
return installDataPacks(DataVersion.getDefault(), fullInstall);
}
public static boolean installDataPacks(IDataFixer fixer, boolean fullInstall) {
if (fixer == null) {
Iris.error("Unable to install datapacks, fixer is null!");
return false;
}
Iris.info("Checking Data Packs...");
DimensionHeight height = new DimensionHeight(fixer);
KList<File> folders = getDatapacksFolder();
KMap<String, KSet<String>> biomes = new KMap<>();
try (Stream<IrisData> stream = allPacks()) {
stream.flatMap(height::merge)
.parallel()
.forEach(dim -> {
Iris.verbose(" Checking Dimension " + dim.getLoadFile().getPath());
dim.installBiomes(fixer, dim::getLoader, folders, biomes.computeIfAbsent(dim.getLoadKey(), k -> new KSet<>()));
dim.installDimensionType(fixer, folders);
});
}
IrisDimension.writeShared(folders, height);
Iris.info("Data Packs Setup!");
return fullInstall && verifyDataPacksPost(IrisSettings.get().getAutoConfiguration().isAutoRestartOnCustomBiomeInstall());
}
private static boolean verifyDataPacksPost(boolean allowRestarting) {
try (Stream<IrisData> stream = allPacks()) {
boolean bad = stream
.map(data -> {
Iris.verbose("Checking Pack: " + data.getDataFolder().getPath());
var loader = data.getDimensionLoader();
return loader.loadAll(loader.getPossibleKeys())
.stream()
.filter(Objects::nonNull)
.map(ServerConfigurator::verifyDataPackInstalled)
.toList()
.contains(false);
})
.toList()
.contains(true);
if (!bad) return false;
}
if (allowRestarting) {
restart();
} else if (INMS.get().supportsDataPacks()) {
Iris.error("============================================================================");
Iris.error(C.ITALIC + "You need to restart your server to properly generate custom biomes.");
Iris.error(C.ITALIC + "By continuing, Iris will use backup biomes in place of the custom biomes.");
Iris.error("----------------------------------------------------------------------------");
Iris.error(C.UNDERLINE + "IT IS HIGHLY RECOMMENDED YOU RESTART THE SERVER BEFORE GENERATING!");
Iris.error("============================================================================");
for (Player i : Bukkit.getOnlinePlayers()) {
if (i.isOp() || i.hasPermission("iris.all")) {
VolmitSender sender = new VolmitSender(i, Iris.instance.getTag("WARNING"));
sender.sendMessage("There are some Iris Packs that have custom biomes in them");
sender.sendMessage("You need to restart your server to use these packs.");
}
}
J.sleep(3000);
}
return true;
}
public static void restart() {
J.s(() -> {
Iris.warn("New data pack entries have been installed in Iris! Restarting server!");
Iris.warn("This will only happen when your pack changes (updates/first time setup)");
Iris.warn("(You can disable this auto restart in iris settings)");
J.s(() -> {
Iris.warn("Looks like the restart command didn't work. Stopping the server instead!");
Bukkit.shutdown();
}, 100);
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "restart");
});
}
public static boolean verifyDataPackInstalled(IrisDimension dimension) {
KSet<String> keys = new KSet<>();
boolean warn = false;
for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) {
if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
keys.add(dimension.getLoadKey() + ":" + j.getId());
}
}
}
String key = worldFolderNameOf(dimension.getLoader());
if (key == null) key = dimension.getLoadKey();
else key += "/" + dimension.getLoadKey();
if (!INMS.get().supportsDataPacks()) {
if (!keys.isEmpty()) {
Iris.warn("===================================================================================");
Iris.warn("Pack " + key + " has " + keys.size() + " custom biome(s). ");
Iris.warn("Your server version does not yet support datapacks for iris.");
Iris.warn("The world will generate these biomes as backup biomes.");
Iris.warn("====================================================================================");
}
return true;
}
for (String i : keys) {
Object o = INMS.get().getCustomBiomeBaseFor(i);
if (o == null) {
Iris.warn("The Biome " + i + " is not registered on the server.");
warn = true;
}
}
if (INMS.get().missingDimensionTypes(dimension.getDimensionTypeKey())) {
Iris.warn("The Dimension Type for " + dimension.getLoadFile() + " is not registered on the server.");
warn = true;
}
if (warn) {
Iris.error("The Pack " + key + " is INCAPABLE of generating custom biomes");
Iris.error("If not done automatically, restart your server before generating with this pack!");
}
return !warn;
}
public static Stream<IrisData> allPacks() {
return Stream.concat(listFiles(Iris.instance.getDataFolder("packs"))
.filter(File::isDirectory)
.filter( base -> {
var content = new File(base, "dimensions").listFiles();
return content != null && content.length > 0;
})
.map(IrisData::get), IrisWorlds.get().getPacks());
}
@Nullable
private static String worldFolderNameOf(IrisData data) {
File pack = data.getDataFolder();
File iris = pack.getParentFile();
if (iris == null || iris.getParentFile() == null || !pack.getName().equals("pack") || !iris.getName().equals("iris")) {
return null;
}
return iris.getParentFile().getName();
}
@SneakyThrows
private static Stream<File> listFiles(File parent) {
if (!parent.isDirectory()) return Stream.empty();
return Files.walk(parent.toPath()).map(Path::toFile);
}
public static class DimensionHeight {
private final IDataFixer fixer;
private final AtomicIntegerArray[] dimensions = new AtomicIntegerArray[3];
public DimensionHeight(IDataFixer fixer) {
this.fixer = fixer;
for (int i = 0; i < 3; i++) {
dimensions[i] = new AtomicIntegerArray(new int[]{
Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE
});
}
}
public Stream<IrisDimension> merge(IrisData data) {
Iris.verbose("Checking Pack: " + data.getDataFolder().getPath());
var loader = data.getDimensionLoader();
return loader.loadAll(loader.getPossibleKeys())
.stream()
.filter(Objects::nonNull)
.peek(this::merge);
}
public void merge(IrisDimension dimension) {
AtomicIntegerArray array = dimensions[dimension.getBaseDimension().ordinal()];
array.updateAndGet(0, min -> Math.min(min, dimension.getMinHeight()));
array.updateAndGet(1, max -> Math.max(max, dimension.getMaxHeight()));
array.updateAndGet(2, logical -> Math.max(logical, dimension.getLogicalHeight()));
}
public String[] jsonStrings() {
var dims = IDataFixer.Dimension.values();
var arr = new String[3];
for (int i = 0; i < 3; i++) {
arr[i] = jsonString(dims[i]);
}
return arr;
}
public String jsonString(IDataFixer.Dimension dimension) {
var data = dimensions[dimension.ordinal()];
int minY = data.get(0);
int maxY = data.get(1);
int logicalHeight = data.get(2);
if (minY == Integer.MAX_VALUE || maxY == Integer.MIN_VALUE || Integer.MIN_VALUE == logicalHeight)
return null;
return fixer.createDimension(dimension, minY, maxY - minY, logicalHeight, null).toString(4);
}
}
}
@@ -0,0 +1,580 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.commands;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.volmit.iris.Iris;
import com.volmit.iris.core.ServerConfigurator;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.core.nms.INMS;
import com.volmit.iris.core.nms.datapack.DataVersion;
import com.volmit.iris.core.service.IrisEngineSVC;
import com.volmit.iris.core.tools.IrisPackBenchmarking;
import com.volmit.iris.core.tools.IrisToolbelt;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisDimension;
import com.volmit.iris.engine.object.IrisPosition;
import com.volmit.iris.engine.object.annotations.Snippet;
import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.collection.KSet;
import com.volmit.iris.util.context.IrisContext;
import com.volmit.iris.engine.object.IrisJigsawStructurePlacement;
import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.decree.DecreeExecutor;
import com.volmit.iris.util.decree.DecreeOrigin;
import com.volmit.iris.util.decree.annotations.Decree;
import com.volmit.iris.util.decree.annotations.Param;
import com.volmit.iris.util.decree.specialhandlers.NullableDimensionHandler;
import com.volmit.iris.util.format.C;
import com.volmit.iris.util.format.Form;
import com.volmit.iris.util.io.CountingDataInputStream;
import com.volmit.iris.util.io.IO;
import com.volmit.iris.util.mantle.TectonicPlate;
import com.volmit.iris.util.math.M;
import com.volmit.iris.util.matter.Matter;
import com.volmit.iris.util.nbt.mca.MCAFile;
import com.volmit.iris.util.nbt.mca.MCAUtil;
import com.volmit.iris.util.parallel.MultiBurst;
import com.volmit.iris.util.plugin.VolmitSender;
import com.volmit.iris.util.scheduling.jobs.Job;
import lombok.SneakyThrows;
import net.jpountz.lz4.LZ4BlockInputStream;
import net.jpountz.lz4.LZ4BlockOutputStream;
import net.jpountz.lz4.LZ4FrameInputStream;
import net.jpountz.lz4.LZ4FrameOutputStream;
import org.apache.commons.lang.RandomStringUtils;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.World;
import java.io.*;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
@Decree(name = "Developer", origin = DecreeOrigin.BOTH, description = "Iris World Manager", aliases = {"dev"})
public class CommandDeveloper implements DecreeExecutor {
private CommandTurboPregen turboPregen;
private CommandLazyPregen lazyPregen;
@Decree(description = "Get Loaded TectonicPlates Count", origin = DecreeOrigin.BOTH, sync = true)
public void EngineStatus() {
Iris.service(IrisEngineSVC.class)
.engineStatus(sender());
}
@Decree(description = "Send a test exception to sentry")
public void Sentry() {
Engine engine = engine();
if (engine != null) IrisContext.getOr(engine);
Iris.reportError(new Exception("This is a test"));
}
@Decree(description = "Dev cmd to fix all the broken objects caused by faulty shrinkwarp")
public void fixObjects(
@Param(aliases = "dimension", description = "The dimension type to create the world with")
IrisDimension type
) {
if (type == null) {
sender().sendMessage("Type cant be null?");
return;
}
IrisData dm = IrisData.get(Iris.instance.getDataFolder("packs", type.getLoadKey()));
var loader = dm.getObjectLoader();
var processed = new KMap<String, IrisPosition>();
var objects = loader.getPossibleKeys();
var pieces = dm.getJigsawPieceLoader().getPossibleKeys();
var sender = sender();
sender.sendMessage(C.IRIS + "Found " + objects.length + " objects in " + type.getLoadKey());
sender.sendMessage(C.IRIS + "Found " + pieces.length + " jigsaw pieces in " + type.getLoadKey());
final int total = objects.length;
final AtomicInteger completed = new AtomicInteger();
final AtomicInteger changed = new AtomicInteger();
new Job() {
@Override
public String getName() {
return "Fixing Objects";
}
@Override
public void execute() {
Arrays.stream(pieces).parallel()
.map(dm.getJigsawPieceLoader()::load)
.filter(Objects::nonNull)
.forEach(piece -> {
var offset = processed.compute(piece.getObject(), (key, o) -> {
if (o != null) return o;
var obj = loader.load(key);
if (obj == null) return new IrisPosition();
obj.shrinkwrap();
try {
if (!obj.getShrinkOffset().isZero()) {
changed.incrementAndGet();
obj.write(obj.getLoadFile());
}
completeWork();
} catch (IOException e) {
Iris.error("Failed to write object " + obj.getLoadKey());
e.printStackTrace();
return new IrisPosition();
}
return new IrisPosition(obj.getShrinkOffset());
});
if (offset.getX() == 0 && offset.getY() == 0 && offset.getZ() == 0)
return;
piece.getConnectors().forEach(connector -> connector.setPosition(connector.getPosition().add(offset)));
try {
IO.writeAll(piece.getLoadFile(), dm.getGson().toJson(piece));
} catch (IOException e) {
Iris.error("Failed to write jigsaw piece " + piece.getLoadKey());
e.printStackTrace();
}
});
Arrays.stream(loader.getPossibleKeys()).parallel()
.filter(key -> !processed.containsKey(key))
.map(loader::load)
.forEach(obj -> {
if (obj == null) {
completeWork();
return;
}
obj.shrinkwrap();
if (obj.getShrinkOffset().isZero()) {
completeWork();
return;
}
try {
obj.write(obj.getLoadFile());
completeWork();
changed.incrementAndGet();
} catch (IOException e) {
Iris.error("Failed to write object " + obj.getLoadKey());
e.printStackTrace();
}
});
}
@Override
public void completeWork() {
completed.incrementAndGet();
}
@Override
public int getTotalWork() {
return total;
}
@Override
public int getWorkCompleted() {
return completed.get();
}
}.execute(sender, () -> {
var failed = total - completed.get();
if (failed != 0) sender.sendMessage(C.IRIS + "" + failed + " objects failed!");
if (changed.get() != 0) sender.sendMessage(C.IRIS + "" + changed.get() + " objects had their offsets changed!");
else sender.sendMessage(C.IRIS + "No objects had their offsets changed!");
});
}
@Decree(description = "Test")
public void mantle(@Param(defaultValue = "false") boolean plate, @Param(defaultValue = "21474836474") String name) throws Throwable {
var base = Iris.instance.getDataFile("dump", "pv." + name + ".ttp.lz4b.bin");
var section = Iris.instance.getDataFile("dump", "pv." + name + ".section.bin");
//extractSection(base, section, 5604930, 4397);
if (plate) {
try (var in = CountingDataInputStream.wrap(new BufferedInputStream(new FileInputStream(base)))) {
new TectonicPlate(1088, in, true);
} catch (Throwable e) {
e.printStackTrace();
}
} else Matter.read(section);
if (!TectonicPlate.hasError())
Iris.info("Read " + (plate ? base : section).length() + " bytes from " + (plate ? base : section).getAbsolutePath());
}
private void extractSection(File source, File target, long offset, int length) throws IOException {
var raf = new RandomAccessFile(source, "r");
var bytes = new byte[length];
raf.seek(offset);
raf.readFully(bytes);
raf.close();
Files.write(target.toPath(), bytes);
}
@Decree(description = "Test")
public void dumpThreads() {
try {
File fi = Iris.instance.getDataFile("dump", "td-" + new java.sql.Date(M.ms()) + ".txt");
FileOutputStream fos = new FileOutputStream(fi);
Map<Thread, StackTraceElement[]> f = Thread.getAllStackTraces();
PrintWriter pw = new PrintWriter(fos);
pw.println(Thread.activeCount() + "/" + f.size());
var run = Runtime.getRuntime();
pw.println("Memory:");
pw.println("\tMax: " + run.maxMemory());
pw.println("\tTotal: " + run.totalMemory());
pw.println("\tFree: " + run.freeMemory());
pw.println("\tUsed: " + (run.totalMemory() - run.freeMemory()));
for (Thread i : f.keySet()) {
pw.println("========================================");
pw.println("Thread: '" + i.getName() + "' ID: " + i.getId() + " STATUS: " + i.getState().name());
for (StackTraceElement j : f.get(i)) {
pw.println(" @ " + j.toString());
}
pw.println("========================================");
pw.println();
pw.println();
}
pw.close();
Iris.info("DUMPED! See " + fi.getAbsolutePath());
} catch (Throwable e) {
e.printStackTrace();
}
}
@SneakyThrows
@Decree(description = "Generate Iris structures for all loaded datapack structures")
public void generateStructures(
@Param(description = "The pack to add the generated structures to", aliases = "pack", defaultValue = "null", customHandler = NullableDimensionHandler.class)
IrisDimension dimension,
@Param(description = "Ignore existing structures", defaultValue = "false")
boolean force
) {
var map = INMS.get().collectStructures();
if (map.isEmpty()) {
sender().sendMessage(C.IRIS + "No structures found");
return;
}
sender().sendMessage(C.IRIS + "Found " + map.size() + " structures");
final File dataDir;
final IrisData data;
final Set<String> existingStructures;
final Map<String, Set<String>> snippets;
final File dimensionFile;
final File structuresFolder;
final File snippetsFolder;
var dimensionObj = new JsonObject();
if (dimension == null) {
dataDir = Iris.instance.getDataFolder("structures");
IO.delete(dataDir);
data = IrisData.get(dataDir);
existingStructures = Set.of();
snippets = Map.of();
dimensionFile = new File(dataDir, "structures.json");
} else {
data = dimension.getLoader();
dataDir = data.getDataFolder();
existingStructures = new KSet<>(data.getJigsawStructureLoader().getPossibleKeys());
dimensionObj = data.getGson().fromJson(IO.readAll(dimension.getLoadFile()), JsonObject.class);
snippets = Optional.ofNullable(dimensionObj.getAsJsonArray("jigsawStructures"))
.map(array -> array.asList()
.stream()
.filter(JsonElement::isJsonPrimitive)
.collect(Collectors.toMap(element -> data.getGson()
.fromJson(element, IrisJigsawStructurePlacement.class)
.getStructure(),
element -> Set.of(element.getAsString()),
KSet::merge)))
.orElse(Map.of());
dimensionFile = dimension.getLoadFile();
}
structuresFolder = new File(dataDir, "jigsaw-structures");
snippetsFolder = new File(dataDir, "snippet" + "/" + IrisJigsawStructurePlacement.class.getAnnotation(Snippet.class).value());
var gson = data.getGson();
var jigsawStructures = Optional.ofNullable(dimensionObj.getAsJsonArray("jigsawStructures"))
.orElse(new JsonArray(map.size()));
map.forEach((key, placement) -> {
String loadKey = "datapack/" + key.namespace() + "/" + key.key();
if (existingStructures.contains(loadKey) && !force)
return;
var structures = placement.structures();
var obj = placement.toJson(loadKey);
if (obj == null || structures.isEmpty()) {
sender().sendMessage(C.RED + "Failed to generate hook for " + key);
return;
}
File snippetFile = new File(snippetsFolder, loadKey + ".json");
try {
IO.writeAll(snippetFile, gson.toJson(obj));
} catch (IOException e) {
sender().sendMessage(C.RED + "Failed to generate snippet for " + key);
e.printStackTrace();
return;
}
Set<String> loadKeys = snippets.getOrDefault(loadKey, Set.of(loadKey));
jigsawStructures.asList().removeIf(e -> loadKeys.contains((e.isJsonObject() ? e.getAsJsonObject().get("structure") : e).getAsString()));
jigsawStructures.add("snippet/" + loadKey);
String structureKey;
if (structures.size() > 1) {
KList<String> common = new KList<>();
for (int i = 0; i < structures.size(); i++) {
var tags = structures.get(i).tags();
if (i == 0) common.addAll(tags);
else common.removeIf(tag -> !tags.contains(tag));
}
structureKey = common.isNotEmpty() ? "#" + common.getFirst() : structures.getFirst().key();
} else structureKey = structures.getFirst().key();
JsonArray array = new JsonArray();
if (structures.size() > 1) {
structures.stream()
.flatMap(structure -> {
String[] arr = new String[structure.weight()];
Arrays.fill(arr, structure.key());
return Arrays.stream(arr);
})
.forEach(array::add);
} else array.add(structureKey);
obj = new JsonObject();
obj.addProperty("structureKey", structureKey);
obj.add("datapackStructures", array);
File out = new File(structuresFolder, loadKey + ".json");
out.getParentFile().mkdirs();
try {
IO.writeAll(out, gson.toJson(obj));
} catch (IOException e) {
e.printStackTrace();
}
});
dimensionObj.add("jigsawStructures", jigsawStructures);
IO.writeAll(dimensionFile, gson.toJson(dimensionObj));
data.hotloaded();
}
@Decree(description = "Test")
public void packBenchmark(
@Param(description = "The pack to bench", aliases = {"pack"}, defaultValue = "overworld")
IrisDimension dimension,
@Param(description = "Radius in regions", defaultValue = "2048")
int radius,
@Param(description = "Open GUI while benchmarking", defaultValue = "false")
boolean gui
) {
new IrisPackBenchmarking(dimension, radius, gui);
}
@Decree(description = "Upgrade to another Minecraft version")
public void upgrade(
@Param(description = "The version to upgrade to", defaultValue = "latest") DataVersion version) {
sender().sendMessage(C.GREEN + "Upgrading to " + version.getVersion() + "...");
ServerConfigurator.installDataPacks(version.get(), false);
sender().sendMessage(C.GREEN + "Done upgrading! You can now update your server version to " + version.getVersion());
}
@Decree(description = "test")
public void mca (
@Param(description = "String") String world) {
try {
File[] McaFiles = new File(world, "region").listFiles((dir, name) -> name.endsWith(".mca"));
for (File mca : McaFiles) {
MCAFile MCARegion = MCAUtil.read(mca);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Decree(description = "UnloadChunks for good reasons.")
public void unloadchunks() {
List<World> IrisWorlds = new ArrayList<>();
int chunksUnloaded = 0;
for (World world : Bukkit.getWorlds()) {
try {
if (IrisToolbelt.access(world).getEngine() != null) {
IrisWorlds.add(world);
}
} catch (Exception e) {
// no
}
}
for (World world : IrisWorlds) {
for (Chunk chunk : world.getLoadedChunks()) {
if (chunk.isLoaded()) {
chunk.unload();
chunksUnloaded++;
}
}
}
Iris.info(C.IRIS + "Chunks Unloaded: " + chunksUnloaded);
}
@Decree
public void objects(@Param(defaultValue = "overworld") IrisDimension dimension) {
var loader = dimension.getLoader().getObjectLoader();
var sender = sender();
var keys = loader.getPossibleKeys();
var burst = MultiBurst.burst.burst(keys.length);
AtomicInteger failed = new AtomicInteger();
for (String key : keys) {
burst.queue(() -> {
if (loader.load(key) == null)
failed.incrementAndGet();
});
}
burst.complete();
sender.sendMessage(C.RED + "Failed to load " + failed.get() + " of " + keys.length + " objects");
}
@Decree(description = "Test", aliases = {"ip"})
public void network() {
try {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
for (NetworkInterface ni : Collections.list(networkInterfaces)) {
Iris.info("Display Name: %s", ni.getDisplayName());
Enumeration<InetAddress> inetAddresses = ni.getInetAddresses();
for (InetAddress ia : Collections.list(inetAddresses)) {
Iris.info("IP: %s", ia.getHostAddress());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Decree(description = "Test the compression algorithms")
public void compression(
@Param(description = "base IrisWorld") World world,
@Param(description = "raw TectonicPlate File") String path,
@Param(description = "Algorithm to Test") String algorithm,
@Param(description = "Amount of Tests") int amount,
@Param(description = "Is versioned", defaultValue = "false") boolean versioned) {
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", Bukkit.getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
return;
}
File file = new File(path);
if (!file.exists()) return;
Engine engine = IrisToolbelt.access(world).getEngine();
if(engine != null) {
int height = engine.getTarget().getHeight();
ExecutorService service = Executors.newFixedThreadPool(1);
VolmitSender sender = sender();
service.submit(() -> {
try {
CountingDataInputStream raw = CountingDataInputStream.wrap(new FileInputStream(file));
TectonicPlate plate = new TectonicPlate(height, raw, versioned);
raw.close();
double d1 = 0;
double d2 = 0;
long size = 0;
File folder = new File("tmp");
folder.mkdirs();
for (int i = 0; i < amount; i++) {
File tmp = new File(folder, RandomStringUtils.randomAlphanumeric(10) + "." + algorithm + ".bin");
DataOutputStream dos = createOutput(tmp, algorithm);
long start = System.currentTimeMillis();
plate.write(dos);
dos.close();
d1 += System.currentTimeMillis() - start;
if (size == 0)
size = tmp.length();
start = System.currentTimeMillis();
CountingDataInputStream din = createInput(tmp, algorithm);
new TectonicPlate(height, din, true);
din.close();
d2 += System.currentTimeMillis() - start;
tmp.delete();
}
IO.delete(folder);
sender.sendMessage(algorithm + " is " + Form.fileSize(size) + " big after compression");
sender.sendMessage(algorithm + " Took " + d2/amount + "ms to read");
sender.sendMessage(algorithm + " Took " + d1/amount + "ms to write");
} catch (Throwable e) {
e.printStackTrace();
}
});
service.shutdown();
} else {
Iris.info(C.RED + "Engine is null!");
}
}
private CountingDataInputStream createInput(File file, String algorithm) throws Throwable {
FileInputStream in = new FileInputStream(file);
return CountingDataInputStream.wrap(switch (algorithm) {
case "gzip" -> new GZIPInputStream(in);
case "lz4f" -> new LZ4FrameInputStream(in);
case "lz4b" -> new LZ4BlockInputStream(in);
default -> throw new IllegalStateException("Unexpected value: " + algorithm);
});
}
private DataOutputStream createOutput(File file, String algorithm) throws Throwable {
FileOutputStream out = new FileOutputStream(file);
return new DataOutputStream(switch (algorithm) {
case "gzip" -> new GZIPOutputStream(out);
case "lz4f" -> new LZ4FrameOutputStream(out);
case "lz4b" -> new LZ4BlockOutputStream(out);
default -> throw new IllegalStateException("Unexpected value: " + algorithm);
});
}
}
@@ -46,6 +46,16 @@ public class CommandEdit implements DecreeExecutor {
sender().sendMessage(C.RED + "You must be in a studio world!");
return true;
}
if (GraphicsEnvironment.isHeadless()) {
sender().sendMessage(C.RED + "Cannot open files in headless environments!");
return true;
}
if (!Desktop.isDesktopSupported()) {
sender().sendMessage(C.RED + "Desktop is not supported by this environment!");
return true;
}
return false;
}
@@ -34,7 +34,9 @@ public class CommandFind implements DecreeExecutor {
@Decree(description = "Find a biome")
public void biome(
@Param(description = "The biome to look for")
IrisBiome biome
IrisBiome biome,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
@@ -43,13 +45,15 @@ public class CommandFind implements DecreeExecutor {
return;
}
e.gotoBiome(biome, player());
e.gotoBiome(biome, player(), teleport);
}
@Decree(description = "Find a region")
public void region(
@Param(description = "The region to look for")
IrisRegion region
IrisRegion region,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
@@ -58,13 +62,15 @@ public class CommandFind implements DecreeExecutor {
return;
}
e.gotoRegion(region, player());
e.gotoRegion(region, player(), teleport);
}
@Decree(description = "Find a structure")
public void structure(
@Param(description = "The structure to look for")
IrisJigsawStructure structure
IrisJigsawStructure structure,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
@@ -73,13 +79,15 @@ public class CommandFind implements DecreeExecutor {
return;
}
e.gotoJigsaw(structure, player());
e.gotoJigsaw(structure, player(), teleport);
}
@Decree(description = "Find a point of interest.")
public void poi(
@Param(description = "The type of PoI to look for.")
String type
String type,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
if (e == null) {
@@ -87,13 +95,15 @@ public class CommandFind implements DecreeExecutor {
return;
}
e.gotoPOI(type, player());
e.gotoPOI(type, player(), teleport);
}
@Decree(description = "Find an object")
public void object(
@Param(description = "The object to look for", customHandler = ObjectHandler.class)
String object
String object,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
@@ -102,6 +112,6 @@ public class CommandFind implements DecreeExecutor {
return;
}
e.gotoObject(object, player());
e.gotoObject(object, player(), teleport);
}
}
@@ -0,0 +1,529 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.commands;
import com.volmit.iris.Iris;
import com.volmit.iris.core.IrisSettings;
import com.volmit.iris.core.nms.INMS;
import com.volmit.iris.core.service.StudioSVC;
import com.volmit.iris.core.tools.IrisToolbelt;
import com.volmit.iris.core.world.WorldStorage;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisDimension;
import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.decree.DecreeExecutor;
import com.volmit.iris.util.decree.DecreeOrigin;
import com.volmit.iris.util.decree.annotations.Decree;
import com.volmit.iris.util.decree.annotations.Param;
import com.volmit.iris.util.decree.specialhandlers.NullablePlayerHandler;
import com.volmit.iris.util.format.C;
import com.volmit.iris.util.plugin.VolmitSender;
import com.volmit.iris.util.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import java.io.*;
import java.util.concurrent.atomic.AtomicReference;
import static com.volmit.iris.core.service.EditSVC.deletingWorld;
import static com.volmit.iris.util.misc.ServerProperties.BUKKIT_YML;
import static org.bukkit.Bukkit.getServer;
@Decree(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command")
public class CommandIris implements DecreeExecutor {
private CommandUpdater updater;
private CommandStudio studio;
private CommandPregen pregen;
private CommandSettings settings;
private CommandObject object;
private CommandJigsaw jigsaw;
private CommandWhat what;
private CommandEdit edit;
private CommandFind find;
private CommandDeveloper developer;
public static boolean worldCreation = false;
private static final AtomicReference<Thread> mainWorld = new AtomicReference<>();
String WorldEngine;
String worldNameToCheck = "YourWorldName";
VolmitSender sender = Iris.getSender();
@Decree(description = "Create a new world", aliases = {"+", "c"})
public void create(
@Param(aliases = "world-name", description = "The name of the world to create")
String name,
@Param(aliases = "dimension", description = "The dimension type to create the world with", defaultValue = "default")
IrisDimension type,
@Param(description = "The seed to generate the world with", defaultValue = "1337")
long seed,
@Param(aliases = "main-world", description = "Whether or not to automatically use this world as the main world", defaultValue = "false")
boolean main
) {
if (name.equalsIgnoreCase("iris")) {
sender().sendMessage(C.RED + "You cannot use the world name \"iris\" for creating worlds as Iris uses this directory for studio worlds.");
sender().sendMessage(C.RED + "May we suggest the name \"IrisWorld\" instead?");
return;
}
if (name.equalsIgnoreCase("benchmark")) {
sender().sendMessage(C.RED + "You cannot use the world name \"benchmark\" for creating worlds as Iris uses this directory for Benchmarking Packs.");
sender().sendMessage(C.RED + "May we suggest the name \"IrisWorld\" instead?");
return;
}
if (WorldStorage.get().worldFolder(name).exists()) {
sender().sendMessage(C.RED + "That folder already exists!");
return;
}
try {
worldCreation = true;
IrisToolbelt.createWorld()
.dimension(type.getLoadKey())
.name(name)
.seed(seed)
.sender(sender())
.studio(false)
.create();
if (main) {
WorldStorage storage = WorldStorage.get();
Runtime.getRuntime().addShutdownHook(mainWorld.updateAndGet(old -> {
if (old != null) Runtime.getRuntime().removeShutdownHook(old);
return new Thread(() -> makeMainWorld(storage, name));
}));
}
} catch (Throwable e) {
sender().sendMessage(C.RED + "Exception raised during creation. See the console for more details.");
Iris.error("Exception raised during world creation: " + e.getMessage());
Iris.reportError(e);
worldCreation = false;
return;
}
worldCreation = false;
sender().sendMessage(C.GREEN + "Successfully created your world!");
if (main) sender().sendMessage(C.GREEN + "Your world will automatically be set as the main world when the server restarts.");
}
private static void makeMainWorld(WorldStorage storage, String worldName) {
try {
storage.makeMainWorld(worldName);
} catch (Throwable e) {
Iris.error("Failed to make " + worldName + " the main world!");
e.printStackTrace();
}
}
@Decree(description = "Teleport to another world", aliases = {"tp"}, sync = true)
public void teleport(
@Param(description = "World to teleport to")
World world,
@Param(description = "Player to teleport", defaultValue = "---", customHandler = NullablePlayerHandler.class)
Player player
) {
if (player == null && sender().isPlayer())
player = sender().player();
final Player target = player;
if (target == null) {
sender().sendMessage(C.RED + "The specified player does not exist.");
return;
}
new BukkitRunnable() {
@Override
public void run() {
target.teleport(world.getSpawnLocation());
new VolmitSender(target).sendMessage(C.GREEN + "You have been teleported to " + world.getName() + ".");
}
}.runTask(Iris.instance);
}
@Decree(description = "Print version information")
public void version() {
sender().sendMessage(C.GREEN + "Iris v" + Iris.instance.getDescription().getVersion() + " by Volmit Software");
}
/*
/todo
@Decree(description = "Benchmark a pack", origin = DecreeOrigin.CONSOLE)
public void packbenchmark(
@Param(description = "Dimension to benchmark")
IrisDimension type
) throws InterruptedException {
BenchDimension = type.getLoadKey();
IrisPackBenchmarking.runBenchmark();
} */
@Decree(description = "Print world height information", origin = DecreeOrigin.PLAYER)
public void height() {
if (sender().isPlayer()) {
sender().sendMessage(C.GREEN + "" + sender().player().getWorld().getMinHeight() + " to " + sender().player().getWorld().getMaxHeight());
sender().sendMessage(C.GREEN + "Total Height: " + (sender().player().getWorld().getMaxHeight() - sender().player().getWorld().getMinHeight()));
} else {
World mainWorld = getServer().getWorlds().get(0);
Iris.info(C.GREEN + "" + mainWorld.getMinHeight() + " to " + mainWorld.getMaxHeight());
Iris.info(C.GREEN + "Total Height: " + (mainWorld.getMaxHeight() - mainWorld.getMinHeight()));
}
}
@Decree(description = "QOL command to open a overworld studio world.", sync = true)
public void so() {
sender().sendMessage(C.GREEN + "Opening studio for the \"Overworld\" pack (seed: 1337)");
Iris.service(StudioSVC.class).open(sender(), 1337, "overworld");
}
@Decree(description = "Check access of all worlds.", aliases = {"accesslist"})
public void worlds() {
KList<World> IrisWorlds = new KList<>();
KList<World> BukkitWorlds = new KList<>();
for (World w : Bukkit.getServer().getWorlds()) {
try {
Engine engine = IrisToolbelt.access(w).getEngine();
if (engine != null) {
IrisWorlds.add(w);
}
} catch (Exception e) {
BukkitWorlds.add(w);
}
}
if (sender().isPlayer()) {
sender().sendMessage(C.BLUE + "Iris Worlds: ");
for (World IrisWorld : IrisWorlds.copy()) {
sender().sendMessage(C.IRIS + "- " +IrisWorld.getName());
}
sender().sendMessage(C.GOLD + "Bukkit Worlds: ");
for (World BukkitWorld : BukkitWorlds.copy()) {
sender().sendMessage(C.GRAY + "- " +BukkitWorld.getName());
}
} else {
Iris.info(C.BLUE + "Iris Worlds: ");
for (World IrisWorld : IrisWorlds.copy()) {
Iris.info(C.IRIS + "- " +IrisWorld.getName());
}
Iris.info(C.GOLD + "Bukkit Worlds: ");
for (World BukkitWorld : BukkitWorlds.copy()) {
Iris.info(C.GRAY + "- " +BukkitWorld.getName());
}
}
}
@Decree(description = "Remove an Iris world", aliases = {"del", "rm", "delete"}, sync = true)
public void remove(
@Param(description = "The world to remove")
World world,
@Param(description = "Whether to also remove the folder (if set to false, just does not load the world)", defaultValue = "true")
boolean delete
) {
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
return;
}
sender().sendMessage(C.GREEN + "Removing world: " + world.getName());
if (!IrisToolbelt.evacuate(world)) {
sender().sendMessage(C.RED + "Failed to evacuate world: " + world.getName());
return;
}
if (!Bukkit.unloadWorld(world, false)) {
sender().sendMessage(C.RED + "Failed to unload world: " + world.getName());
return;
}
try {
if (IrisToolbelt.removeWorld(world)) {
sender().sendMessage(C.GREEN + "Successfully removed " + world.getName() + " from bukkit.yml");
} else {
sender().sendMessage(C.YELLOW + "Looks like the world was already removed from bukkit.yml");
}
} catch (IOException e) {
sender().sendMessage(C.RED + "Failed to save bukkit.yml because of " + e.getMessage());
e.printStackTrace();
}
IrisToolbelt.evacuate(world, "Deleting world");
deletingWorld = true;
if (!delete) {
deletingWorld = false;
return;
}
VolmitSender sender = sender();
J.a(() -> {
int retries = 12;
if (deleteDirectory(world.getWorldFolder())) {
sender.sendMessage(C.GREEN + "Successfully removed world folder");
} else {
while(true){
if (deleteDirectory(world.getWorldFolder())){
sender.sendMessage(C.GREEN + "Successfully removed world folder");
break;
}
retries--;
if (retries == 0){
sender.sendMessage(C.RED + "Failed to remove world folder");
break;
}
J.sleep(3000);
}
}
deletingWorld = false;
});
}
public static boolean deleteDirectory(File dir) {
if (dir.isDirectory()) {
File[] children = dir.listFiles();
for (int i = 0; i < children.length; i++) {
boolean success = deleteDirectory(children[i]);
if (!success) {
return false;
}
}
}
return dir.delete();
}
@Decree(description = "Set aura spins")
public void aura(
@Param(description = "The h color value", defaultValue = "-20")
int h,
@Param(description = "The s color value", defaultValue = "7")
int s,
@Param(description = "The b color value", defaultValue = "8")
int b
) {
IrisSettings.get().getGeneral().setSpinh(h);
IrisSettings.get().getGeneral().setSpins(s);
IrisSettings.get().getGeneral().setSpinb(b);
IrisSettings.get().forceSave();
sender().sendMessage("<rainbow>Aura Spins updated to " + h + " " + s + " " + b);
}
@Decree(description = "Bitwise calculations")
public void bitwise(
@Param(description = "The first value to run calculations on")
int value1,
@Param(description = "The operator: | & ^ ≺≺ ≻≻ %")
String operator,
@Param(description = "The second value to run calculations on")
int value2
) {
Integer v = null;
switch (operator) {
case "|" -> v = value1 | value2;
case "&" -> v = value1 & value2;
case "^" -> v = value1 ^ value2;
case "%" -> v = value1 % value2;
case ">>" -> v = value1 >> value2;
case "<<" -> v = value1 << value2;
}
if (v == null) {
sender().sendMessage(C.RED + "The operator you entered: (" + operator + ") is invalid!");
return;
}
sender().sendMessage(C.GREEN + "" + value1 + " " + C.GREEN + operator.replaceAll("<", "≺").replaceAll(">", "≻").replaceAll("%", "%") + " " + C.GREEN + value2 + C.GREEN + " returns " + C.GREEN + v);
}
@Decree(description = "Toggle debug")
public void debug(
@Param(name = "on", description = "Whether or not debug should be on", defaultValue = "other")
Boolean on
) {
boolean to = on == null ? !IrisSettings.get().getGeneral().isDebug() : on;
IrisSettings.get().getGeneral().setDebug(to);
IrisSettings.get().forceSave();
sender().sendMessage(C.GREEN + "Set debug to: " + to);
}
//TODO fix pack trimming
@Decree(description = "Download a project.", aliases = "dl")
public void download(
@Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project")
String pack,
@Param(name = "branch", description = "The branch to download from", defaultValue = "main")
String branch,
//@Param(name = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false")
//boolean trim,
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false")
boolean overwrite
) {
boolean trim = false;
sender().sendMessage(C.GREEN + "Downloading pack: " + pack + "/" + branch + (trim ? " trimmed" : "") + (overwrite ? " overwriting" : ""));
if (pack.equals("overworld")) {
String url = "https://github.com/IrisDimensions/overworld/releases/download/" + INMS.OVERWORLD_TAG + "/overworld.zip";
Iris.service(StudioSVC.class).downloadRelease(sender(), url, trim, overwrite);
} else {
Iris.service(StudioSVC.class).downloadSearch(sender(), "IrisDimensions/" + pack + "/" + branch, trim, overwrite);
}
}
@Decree(description = "Get metrics for your world", aliases = "measure", origin = DecreeOrigin.PLAYER)
public void metrics() {
if (!IrisToolbelt.isIrisWorld(world())) {
sender().sendMessage(C.RED + "You must be in an Iris world");
return;
}
sender().sendMessage(C.GREEN + "Sending metrics...");
engine().printMetrics(sender());
}
@Decree(description = "Reload configuration file (this is also done automatically)")
public void reload() {
IrisSettings.invalidate();
IrisSettings.get();
sender().sendMessage(C.GREEN + "Hotloaded settings");
}
@Decree(description = "Update the pack of a world (UNSAFE!)", name = "^world", aliases = "update-world")
public void updateWorld(
@Param(description = "The world to update", contextual = true)
World world,
@Param(description = "The pack to install into the world", contextual = true, aliases = "dimension")
IrisDimension pack,
@Param(description = "Make sure to make a backup & read the warnings first!", defaultValue = "false", aliases = "c")
boolean confirm,
@Param(description = "Should Iris download the pack again for you", defaultValue = "false", name = "fresh-download", aliases = {"fresh", "new"})
boolean freshDownload
) {
if (!confirm) {
sender().sendMessage(new String[]{
C.RED + "You should always make a backup before using this",
C.YELLOW + "Issues caused by this can be, but are not limited to:",
C.YELLOW + " - Broken chunks (cut-offs) between old and new chunks (before & after the update)",
C.YELLOW + " - Regenerated chunks that do not fit in with the old chunks",
C.YELLOW + " - Structures not spawning again when regenerating",
C.YELLOW + " - Caves not lining up",
C.YELLOW + " - Terrain layers not lining up",
C.RED + "Now that you are aware of the risks, and have made a back-up:",
C.RED + "/iris ^world " + world.getName() + " " + pack.getLoadKey() + " confirm=true"
});
return;
}
File folder = world.getWorldFolder();
folder.mkdirs();
if (freshDownload) {
Iris.service(StudioSVC.class).downloadSearch(sender(), pack.getLoadKey(), false, true);
}
Iris.service(StudioSVC.class).installIntoWorld(sender(), pack.getLoadKey(), folder);
}
@Decree(description = "Unload an Iris World", origin = DecreeOrigin.PLAYER, sync = true)
public void unloadWorld(
@Param(description = "The world to unload")
World world
) {
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
return;
}
sender().sendMessage(C.GREEN + "Unloading world: " + world.getName());
try {
IrisToolbelt.evacuate(world);
Bukkit.unloadWorld(world, false);
sender().sendMessage(C.GREEN + "World unloaded successfully.");
} catch (Exception e) {
sender().sendMessage(C.RED + "Failed to unload the world: " + e.getMessage());
e.printStackTrace();
}
}
@Decree(description = "Load an Iris World", origin = DecreeOrigin.PLAYER, sync = true, aliases = {"import"})
public void loadWorld(
@Param(description = "The name of the world to load")
String world
) {
World worldloaded = Bukkit.getWorld(world);
WorldStorage.get().adoptLegacyData(world);
worldNameToCheck = world;
boolean worldExists = doesWorldExist(worldNameToCheck);
WorldEngine = world;
if (!worldExists) {
sender().sendMessage(C.YELLOW + world + " Doesnt exist on the server.");
return;
}
File directory = new File(WorldStorage.get().worldFolder(world), "iris/pack/dimensions");
String dimension = null;
if (directory.exists() && directory.isDirectory()) {
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile()) {
String fileName = file.getName();
if (fileName.endsWith(".json")) {
dimension = fileName.substring(0, fileName.length() - 5);
sender().sendMessage(C.BLUE + "Generator: " + dimension);
}
}
}
}
} else {
sender().sendMessage(C.GOLD + world + " is not an iris world.");
return;
}
sender().sendMessage(C.GREEN + "Loading world: " + world);
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
String gen = "Iris:" + dimension;
ConfigurationSection section = yml.contains("worlds") ? yml.getConfigurationSection("worlds") : yml.createSection("worlds");
if (!section.contains(world)) {
section.createSection(world).set("generator", gen);
try {
yml.save(BUKKIT_YML);
Iris.info("Registered \"" + world + "\" in bukkit.yml");
} catch (IOException e) {
Iris.error("Failed to update bukkit.yml!");
e.printStackTrace();
return;
}
}
Iris.instance.checkForBukkitWorlds(world::equals);
sender().sendMessage(C.GREEN + world + " loaded successfully.");
}
@Decree(description = "Evacuate an iris world", origin = DecreeOrigin.PLAYER, sync = true)
public void evacuate(
@Param(description = "Evacuate the world")
World world
) {
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
return;
}
sender().sendMessage(C.GREEN + "Evacuating world" + world.getName());
IrisToolbelt.evacuate(world);
}
boolean doesWorldExist(String worldName) {
File worldDirectory = WorldStorage.get().worldFolder(worldName);
return worldDirectory.exists() && worldDirectory.isDirectory();
}
}
@@ -21,6 +21,7 @@ package com.volmit.iris.core.commands;
import com.volmit.iris.Iris;
import com.volmit.iris.core.edit.JigsawEditor;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.engine.framework.placer.WorldObjectPlacer;
import com.volmit.iris.engine.jigsaw.PlannedStructure;
import com.volmit.iris.engine.object.IrisJigsawPiece;
import com.volmit.iris.engine.object.IrisJigsawStructure;
@@ -34,6 +35,7 @@ import com.volmit.iris.util.decree.specialhandlers.ObjectHandler;
import com.volmit.iris.util.format.C;
import com.volmit.iris.util.format.Form;
import com.volmit.iris.util.math.RNG;
import com.volmit.iris.util.plugin.VolmitSender;
import com.volmit.iris.util.scheduling.PrecisionStopwatch;
import java.io.File;
@@ -46,7 +48,7 @@ public class CommandJigsaw implements DecreeExecutor {
IrisJigsawPiece piece
) {
File dest = piece.getLoadFile();
new JigsawEditor(player(), piece, IrisData.loadAnyObject(piece.getObject()), dest);
new JigsawEditor(player(), piece, IrisData.loadAnyObject(piece.getObject(), data()), dest);
}
@Decree(description = "Place a jigsaw structure")
@@ -55,9 +57,16 @@ public class CommandJigsaw implements DecreeExecutor {
IrisJigsawStructure structure
) {
PrecisionStopwatch p = PrecisionStopwatch.start();
PlannedStructure ps = new PlannedStructure(structure, new IrisPosition(player().getLocation()), new RNG());
sender().sendMessage(C.GREEN + "Generated " + ps.getPieces().size() + " pieces in " + Form.duration(p.getMilliseconds(), 2));
ps.place(world());
try {
var world = world();
WorldObjectPlacer placer = new WorldObjectPlacer(world);
PlannedStructure ps = new PlannedStructure(structure, new IrisPosition(player().getLocation().add(0, world.getMinHeight(), 0)), new RNG(), true);
VolmitSender sender = sender();
sender.sendMessage(C.GREEN + "Generated " + ps.getPieces().size() + " pieces in " + Form.duration(p.getMilliseconds(), 2));
ps.place(placer, failed -> sender.sendMessage(failed ? C.GREEN + "Placed the structure!" : C.RED + "Failed to place the structure!"));
} catch (IllegalArgumentException e) {
sender().sendMessage(C.RED + "Failed to place the structure: " + e.getMessage());
}
}
@Decree(description = "Create a jigsaw piece")
@@ -69,7 +78,7 @@ public class CommandJigsaw implements DecreeExecutor {
@Param(description = "The object to use for this piece", customHandler = ObjectHandler.class)
String object
) {
IrisObject o = IrisData.loadAnyObject(object);
IrisObject o = IrisData.loadAnyObject(object, data());
if (object == null) {
sender().sendMessage(C.RED + "Failed to find existing object");
@@ -0,0 +1,121 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.commands;
import com.volmit.iris.Iris;
import com.volmit.iris.core.IrisSettings;
import com.volmit.iris.core.gui.PregeneratorJob;
import com.volmit.iris.core.pregenerator.LazyPregenerator;
import com.volmit.iris.core.pregenerator.PregenTask;
import com.volmit.iris.core.tools.IrisToolbelt;
import com.volmit.iris.util.decree.DecreeExecutor;
import com.volmit.iris.util.decree.annotations.Decree;
import com.volmit.iris.util.decree.annotations.Param;
import com.volmit.iris.util.format.C;
import com.volmit.iris.util.math.Position2;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.util.Vector;
import java.io.File;
import java.io.IOException;
@Decree(name = "lazypregen", aliases = "lazy", description = "Pregenerate your Iris worlds!")
public class CommandLazyPregen implements DecreeExecutor {
public String worldName;
@Decree(description = "Pregenerate a world")
public void start(
@Param(description = "The radius of the pregen in blocks", aliases = "size")
int radius,
@Param(description = "The world to pregen", contextual = true)
World world,
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0")
Vector center,
@Param(aliases = "maxcpm", description = "Limit the chunks per minute the pregen will generate", defaultValue = "999999999")
int cpm,
@Param(aliases = "silent", description = "Silent generation", defaultValue = "false")
boolean silent
) {
worldName = world.getName();
File worldDirectory = world.getWorldFolder();
File lazyFile = new File(worldDirectory, "lazygen.json");
if (lazyFile.exists()) {
sender().sendMessage(C.BLUE + "Lazy pregen is already in progress");
Iris.info(C.YELLOW + "Lazy pregen is already in progress");
return;
}
try {
if (sender().isPlayer() && access() == null) {
sender().sendMessage(C.RED + "The engine access for this world is null!");
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
}
LazyPregenerator.LazyPregenJob pregenJob = LazyPregenerator.LazyPregenJob.builder()
.world(worldName)
.healingPosition(0)
.healing(false)
.chunksPerMinute(cpm)
.radiusBlocks(radius)
.position(0)
.silent(silent)
.build();
File lazyGenFile = new File(worldDirectory, "lazygen.json");
LazyPregenerator pregenerator = new LazyPregenerator(pregenJob, lazyGenFile);
pregenerator.start();
String msg = C.GREEN + "LazyPregen started in " + C.GOLD + worldName + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
sender().sendMessage(msg);
Iris.info(msg);
} catch (Throwable e) {
sender().sendMessage(C.RED + "Epic fail. See console.");
Iris.reportError(e);
e.printStackTrace();
}
}
@Decree(description = "Stop the active pregeneration task", aliases = "x")
public void stop(
@Param(aliases = "world", description = "The world to pause")
World world
) throws IOException {
if (LazyPregenerator.getInstance() != null) {
LazyPregenerator.getInstance().shutdownInstance(world);
sender().sendMessage(C.LIGHT_PURPLE + "Closed lazygen instance for " + world.getName());
} else {
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
}
}
@Decree(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"})
public void pause(
@Param(aliases = "world", description = "The world to pause")
World world
) {
if (LazyPregenerator.getInstance() != null) {
LazyPregenerator.getInstance().setPausedLazy(world);
sender().sendMessage(C.GREEN + "Paused/unpaused Lazy Pregen, now: " + (LazyPregenerator.getInstance().isPausedLazy(world) ? "Paused" : "Running") + ".");
} else {
sender().sendMessage(C.YELLOW + "No active Lazy Pregen tasks to pause/unpause.");
}
}
}
@@ -24,9 +24,12 @@ import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.core.service.ObjectSVC;
import com.volmit.iris.core.service.StudioSVC;
import com.volmit.iris.core.service.WandSVC;
import com.volmit.iris.core.tools.IrisConverter;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.*;
import com.volmit.iris.util.data.Cuboid;
import com.volmit.iris.util.data.IrisCustomData;
import com.volmit.iris.util.data.registry.Materials;
import com.volmit.iris.util.decree.DecreeExecutor;
import com.volmit.iris.util.decree.DecreeOrigin;
import com.volmit.iris.util.decree.annotations.Decree;
@@ -35,11 +38,8 @@ import com.volmit.iris.util.decree.specialhandlers.ObjectHandler;
import com.volmit.iris.util.format.C;
import com.volmit.iris.util.math.Direction;
import com.volmit.iris.util.math.RNG;
import com.volmit.iris.util.scheduling.Queue;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.block.TileState;
import org.bukkit.block.data.BlockData;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;
@@ -52,7 +52,7 @@ import java.util.*;
@Decree(name = "object", aliases = "o", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris object manipulation")
public class CommandObject implements DecreeExecutor {
private static final Set<Material> skipBlocks = Set.of(Material.GRASS, Material.SNOW, Material.VINE, Material.TORCH, Material.DEAD_BUSH,
private static final Set<Material> skipBlocks = Set.of(Materials.GRASS, Material.SNOW, Material.VINE, Material.TORCH, Material.DEAD_BUSH,
Material.POPPY, Material.DANDELION);
public static IObjectPlacer createPlacer(World world, Map<Block, BlockData> futureBlockChanges) {
@@ -77,7 +77,10 @@ public class CommandObject implements DecreeExecutor {
futureBlockChanges.put(block, block.getBlockData());
block.setBlockData(d);
if (d instanceof IrisCustomData data) {
block.setBlockData(data.getBase(), false);
Iris.warn("Tried to place custom block at " + x + ", " + y + ", " + z + " which is not supported!");
} else block.setBlockData(d, false);
}
@Override
@@ -116,10 +119,18 @@ public class CommandObject implements DecreeExecutor {
}
@Override
public void setTile(int xx, int yy, int zz, TileData<? extends TileState> tile) {
BlockState state = world.getBlockAt(xx, yy, zz).getState();
tile.toBukkitTry(state);
state.update();
public void setTile(int xx, int yy, int zz, TileData tile) {
tile.toBukkitTry(world.getBlockAt(xx, yy, zz));
}
@Override
public <T> void setData(int xx, int yy, int zz, T data) {
}
@Override
public <T> T getData(int xx, int yy, int zz, Class<T> t) {
return null;
}
@Override
@@ -134,11 +145,11 @@ public class CommandObject implements DecreeExecutor {
@Param(description = "The object to analyze", customHandler = ObjectHandler.class)
String object
) {
IrisObject o = IrisData.loadAnyObject(object);
IrisObject o = IrisData.loadAnyObject(object, data());
sender().sendMessage("Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD() + "");
sender().sendMessage("Blocks Used: " + NumberFormat.getIntegerInstance().format(o.getBlocks().size()));
Queue<BlockData> queue = o.getBlocks().enqueueValues();
var queue = o.getBlocks().values();
Map<Material, Set<BlockData>> unsorted = new HashMap<>();
Map<BlockData, Integer> amounts = new HashMap<>();
Map<Material, Integer> materials = new HashMap<>();
@@ -197,6 +208,30 @@ public class CommandObject implements DecreeExecutor {
}
}
@Decree(description = "Shrink an object to its minimum size")
public void shrink(@Param(description = "The object to shrink", customHandler = ObjectHandler.class) String object) {
IrisObject o = IrisData.loadAnyObject(object, data());
sender().sendMessage("Current Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD());
o.shrinkwrap();
sender().sendMessage("New Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD());
try {
o.write(o.getLoadFile());
} catch (IOException e) {
sender().sendMessage("Failed to save object " + o.getLoadFile() + ": " + e.getMessage());
e.printStackTrace();
}
}
@Decree(description = "Convert .schem files in the 'convert' folder to .iob files.")
public void convert () {
try {
IrisConverter.convertSchematics(sender());
} catch (Exception e) {
e.printStackTrace();
}
}
@Decree(description = "Get a powder that reveals objects", studio = true, aliases = "d")
public void dust() {
player().getInventory().addItem(WandSVC.createDust());
@@ -215,7 +250,8 @@ public class CommandObject implements DecreeExecutor {
Location[] b = WandSVC.getCuboid(player());
if (b == null) {
if (b == null || b[0] == null || b[1] == null) {
sender().sendMessage("No area selected.");
return;
}
Location a1 = b[0].clone();
@@ -298,7 +334,7 @@ public class CommandObject implements DecreeExecutor {
// @Param(description = "The scale interpolator to use", defaultValue = "none")
// IrisObjectPlacementScaleInterpolator interpolator
) {
IrisObject o = IrisData.loadAnyObject(object);
IrisObject o = IrisData.loadAnyObject(object, data());
double maxScale = Double.max(10 - o.getBlocks().size() / 10000d, 1);
if (scale > maxScale) {
sender().sendMessage(C.YELLOW + "Indicated scale exceeds maximum. Downscaled to maximum: " + maxScale);
@@ -341,7 +377,7 @@ public class CommandObject implements DecreeExecutor {
}
}
} else {
sender().sendMessage("Placed " + object);
sender().sendMessage(C.IRIS + "Placed " + object);
}
}
@@ -352,9 +388,11 @@ public class CommandObject implements DecreeExecutor {
@Param(description = "The file to store it in, can use / for subfolders")
String name,
@Param(description = "Overwrite existing object files", defaultValue = "false", aliases = "force")
boolean overwrite
boolean overwrite,
@Param(description = "Use legacy TileState serialization if possible", defaultValue = "true")
boolean legacy
) {
IrisObject o = WandSVC.createSchematic(player());
IrisObject o = WandSVC.createSchematic(player(), legacy);
if (o == null) {
sender().sendMessage(C.YELLOW + "You need to hold your wand!");
@@ -368,7 +406,7 @@ public class CommandObject implements DecreeExecutor {
return;
}
try {
o.write(file);
o.write(file, sender());
} catch (IOException e) {
sender().sendMessage(C.RED + "Failed to save object because of an IOException: " + e.getMessage());
Iris.reportError(e);
@@ -389,6 +427,10 @@ public class CommandObject implements DecreeExecutor {
}
Location[] b = WandSVC.getCuboid(player());
if (b == null || b[0] == null || b[1] == null) {
sender().sendMessage("No area selected.");
return;
}
Location a1 = b[0].clone();
Location a2 = b[1].clone();
Direction d = Direction.closest(player().getLocation().getDirection()).reverse();
@@ -413,7 +455,7 @@ public class CommandObject implements DecreeExecutor {
ObjectSVC service = Iris.service(ObjectSVC.class);
int actualReverts = Math.min(service.getUndos().size(), amount);
service.revertChanges(actualReverts);
sender().sendMessage("Reverted " + actualReverts + " pastes!");
sender().sendMessage(C.BLUE + "Reverted " + actualReverts + C.BLUE +" pastes!");
}
@Decree(description = "Gets an object wand and grabs the current WorldEdit selection.", aliases = "we", origin = DecreeOrigin.PLAYER, studio = true)
@@ -449,6 +491,10 @@ public class CommandObject implements DecreeExecutor {
}
Location[] b = WandSVC.getCuboid(player());
if (b == null || b[0] == null || b[1] == null) {
sender().sendMessage("No area selected.");
return;
}
Location a1 = b[0].clone();
Location a2 = b[1].clone();
Location a1x = b[0].clone();
@@ -496,6 +542,10 @@ public class CommandObject implements DecreeExecutor {
}
Location[] b = WandSVC.getCuboid(player());
if (b == null || b[0] == null || b[1] == null) {
sender().sendMessage("No area selected.");
return;
}
b[0].add(new Vector(0, 1, 0));
b[1].add(new Vector(0, 1, 0));
Location a1 = b[0].clone();
@@ -39,20 +39,22 @@ public class CommandPregen implements DecreeExecutor {
@Param(description = "The world to pregen", contextual = true)
World world,
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0")
Vector center
) {
Vector center,
@Param(description = "Open the Iris pregen gui", defaultValue = "true")
boolean gui
) {
try {
if (sender().isPlayer() && access() == null) {
sender().sendMessage(C.RED + "The engine access for this world is null!");
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
}
radius = Math.max(radius, 1024);
int w = (radius >> 9 + 1) * 2;
IrisToolbelt.pregenerate(PregenTask
.builder()
.center(new Position2(center))
.width(w)
.height(w)
.center(new Position2(center.getBlockX(), center.getBlockZ()))
.gui(gui)
.radiusX(radius)
.radiusZ(radius)
.build(), world);
String msg = C.GREEN + "Pregen started in " + C.GOLD + world.getName() + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
sender().sendMessage(msg);
@@ -67,7 +69,7 @@ public class CommandPregen implements DecreeExecutor {
@Decree(description = "Stop the active pregeneration task", aliases = "x")
public void stop() {
if (PregeneratorJob.shutdownInstance()) {
sender().sendMessage(C.GREEN + "Stopped pregeneration task");
Iris.info( C.BLUE + "Finishing up mca region...");
} else {
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
}
@@ -20,8 +20,10 @@ package com.volmit.iris.core.commands;
import com.volmit.iris.Iris;
import com.volmit.iris.core.IrisSettings;
import com.volmit.iris.core.gui.GuiHost;
import com.volmit.iris.core.gui.MapWindow;
import com.volmit.iris.core.gui.NoiseExplorerGUI;
import com.volmit.iris.core.gui.VisionGUI;
import com.volmit.iris.core.gui.PaletteExplorerStart;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.core.project.IrisProject;
import com.volmit.iris.core.service.ConversionSVC;
@@ -29,28 +31,35 @@ import com.volmit.iris.core.service.StudioSVC;
import com.volmit.iris.core.tools.IrisToolbelt;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.*;
import com.volmit.iris.engine.platform.PlatformChunkGenerator;
import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.collection.KSet;
import com.volmit.iris.util.decree.DecreeContext;
import com.volmit.iris.util.decree.DecreeExecutor;
import com.volmit.iris.util.decree.DecreeOrigin;
import com.volmit.iris.util.decree.annotations.Decree;
import com.volmit.iris.util.decree.annotations.Param;
import com.volmit.iris.util.format.C;
import com.volmit.iris.util.format.Form;
import com.volmit.iris.util.function.Function2;
import com.volmit.iris.util.function.NoiseProvider;
import com.volmit.iris.util.interpolation.InterpolationMethod;
import com.volmit.iris.util.io.IO;
import com.volmit.iris.util.json.JSONArray;
import com.volmit.iris.util.json.JSONObject;
import com.volmit.iris.util.mantle.MantleChunk;
import com.volmit.iris.util.math.M;
import com.volmit.iris.util.math.Position2;
import com.volmit.iris.util.math.RNG;
import com.volmit.iris.util.math.Spiraler;
import com.volmit.iris.util.noise.CNG;
import com.volmit.iris.util.parallel.MultiBurst;
import com.volmit.iris.util.parallel.SyncExecutor;
import com.volmit.iris.util.plugin.VolmitSender;
import com.volmit.iris.util.scheduling.J;
import com.volmit.iris.util.scheduling.O;
import com.volmit.iris.util.scheduling.PrecisionStopwatch;
import io.papermc.lib.PaperLib;
import com.volmit.iris.util.scheduling.jobs.ParallelRadiusJob;
import org.bukkit.*;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
@@ -65,31 +74,36 @@ import java.nio.file.Files;
import java.nio.file.attribute.FileTime;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Date;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
@Decree(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", studio = true)
public class CommandStudio implements DecreeExecutor {
private CommandFind find;
private CommandEdit edit;
//private CommandDeepSearch deepSearch;
public static String hrf(Duration duration) {
return duration.toString().substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase();
}
//TODO fix pack trimming
@Decree(description = "Download a project.", aliases = "dl")
public void download(
@Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project")
String pack,
@Param(name = "branch", description = "The branch to download from", defaultValue = "master")
String branch,
@Param(name = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false")
boolean trim,
//@Param(name = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false")
//boolean trim,
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false")
boolean overwrite
) {
new CommandIris().download(pack, branch, trim, overwrite);
new CommandIris().download(pack, branch, overwrite);
}
@Decree(description = "Open a new studio world", aliases = "o", sync = true)
@@ -143,12 +157,87 @@ public class CommandStudio implements DecreeExecutor {
sender().sendMessage(C.GREEN + "The \"" + dimension.getName() + "\" pack has version: " + dimension.getVersion());
}
@Decree(name = "regen", description = "Regenerate nearby chunks.", aliases = "rg", sync = true, origin = DecreeOrigin.PLAYER)
public void regen(
@Param(name = "radius", description = "The radius of nearby cunks", defaultValue = "5")
int radius
) {
World world = player().getWorld();
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.RED + "You must be in an Iris World to use regen!");
}
VolmitSender sender = sender();
var loc = player().getLocation().clone();
J.a(() -> {
PlatformChunkGenerator plat = IrisToolbelt.access(world);
Engine engine = plat.getEngine();
DecreeContext.touch(sender);
try (SyncExecutor executor = new SyncExecutor(20);
var service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
) {
int x = loc.getBlockX() >> 4;
int z = loc.getBlockZ() >> 4;
int rad = engine.getMantle().getRadius();
var mantle = engine.getMantle().getMantle();
var chunkMap = new KMap<Position2, MantleChunk>();
ParallelRadiusJob prep = new ParallelRadiusJob(Integer.MAX_VALUE, service) {
@Override
protected void execute(int rX, int rZ) {
if (Math.abs(rX) <= radius && Math.abs(rZ) <= radius) {
mantle.deleteChunk(rX + x, rZ + z);
return;
}
rX += x;
rZ += z;
chunkMap.put(new Position2(rX, rZ), mantle.getChunk(rX, rZ));
mantle.deleteChunk(rX, rZ);
}
@Override
public String getName() {
return "Preparing Mantle";
}
}.retarget(radius + rad, 0, 0);
CountDownLatch pLatch = new CountDownLatch(1);
prep.execute(sender(), pLatch::countDown);
pLatch.await();
ParallelRadiusJob job = new ParallelRadiusJob(Integer.MAX_VALUE, service) {
@Override
protected void execute(int x, int z) {
plat.injectChunkReplacement(world, x, z, executor);
}
@Override
public String getName() {
return "Regenerating";
}
}.retarget(radius, x, z);
CountDownLatch latch = new CountDownLatch(1);
job.execute(sender(), latch::countDown);
latch.await();
chunkMap.forEach((pos, chunk) ->
mantle.getChunk(pos.getX(), pos.getZ()).copyFrom(chunk));
} catch (Throwable e) {
sender().sendMessage("Error while regenerating chunks");
e.printStackTrace();
} finally {
DecreeContext.remove();
}
});
}
@Decree(description = "Convert objects in the \"convert\" folder")
public void convert() {
Iris.service(ConversionSVC.class).check(sender());
//IrisConverter.convertSchematics(sender());
}
@Decree(description = "Execute a script", aliases = "run", origin = DecreeOrigin.PLAYER)
public void execute(
@Param(description = "The script to run")
@@ -157,11 +246,47 @@ public class CommandStudio implements DecreeExecutor {
engine().getExecution().execute(script.getLoadKey());
}
@Decree(description = "Open the noise explorer (External GUI)", aliases = {"nmap", "n"})
@Decree(description = "Open the map on its noise explorer (External GUI)", aliases = {"nmap", "n"})
public void noise() {
if (noGUI()) return;
sender().sendMessage(C.GREEN + "Opening Noise Explorer!");
NoiseExplorerGUI.launch();
Engine playerEngine = engine();
Engine engine = playerEngine != null ? playerEngine : GuiHost.findActiveEngine();
World world = engine == null ? null : engine.getWorld().realWorld();
if (world == null) {
sender().sendMessage(C.RED + "Open a studio or stand in an Iris world to explore its noise!");
return;
}
Location center = playerEngine != null ? player().getLocation() : null;
sender().sendMessage(C.GREEN + "Opening the map on its noise!");
MapWindow.openNoise(new PaletteExplorerStart(
engine,
world.getUID(),
sender().isPlayer() ? player().getUniqueId() : null,
null,
center == null ? 0D : center.getX(),
center == null ? 0D : center.getZ()
));
}
@Decree(description = "Preview the palettes around you (External GUI)", aliases = "pal", origin = DecreeOrigin.PLAYER)
public void palette() {
if (noGUI()) return;
Engine engine = engine();
if (engine == null) {
sender().sendMessage(C.RED + "Stand in an Iris world to preview its palettes!");
return;
}
Location location = player().getLocation();
IrisBiome surfaceBiome = engine.getSurfaceBiome(location.getBlockX(), location.getBlockZ());
sender().sendMessage(C.GREEN + "Opening the map on its palettes!");
MapWindow.openPalettes(new PaletteExplorerStart(
engine,
location.getWorld().getUID(),
player().getUniqueId(),
surfaceBiome == null ? null : surfaceBiome.getLoadKey(),
location.getX(),
location.getZ()
));
}
@Decree(description = "Charges all spawners in the area", aliases = "zzt", origin = DecreeOrigin.PLAYER)
@@ -183,16 +308,7 @@ public class CommandStudio implements DecreeExecutor {
) {
if (noGUI()) return;
sender().sendMessage(C.GREEN + "Opening Noise Explorer!");
Supplier<Function2<Double, Double, Double>> l = () -> {
if (generator == null) {
return (x, z) -> 0D;
}
return (x, z) -> generator.getHeight(x, z, new RNG(seed).nextParallelRNG(3245).lmax());
};
NoiseExplorerGUI.launch(l, "Custom Generator");
NoiseExplorerGUI.launchGeneratorKey(engine(), generator == null ? null : generator.getLoadKey(), generator, seed);
}
@Decree(description = "Hotload a studio", aliases = {"reload", "h"})
@@ -218,7 +334,7 @@ public class CommandStudio implements DecreeExecutor {
Inventory inv = Bukkit.createInventory(null, 27 * 2);
try {
engine().addItems(true, inv, RNG.r, tables, InventorySlotType.STORAGE, player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ(), 1);
engine().addItems(true, inv, RNG.r, tables, InventorySlotType.STORAGE, player().getWorld(), player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ(), 1);
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(C.RED + "Cannot add items to virtual inventory because of: " + e.getMessage());
@@ -229,11 +345,15 @@ public class CommandStudio implements DecreeExecutor {
O<Integer> ta = new O<>();
ta.set(-1);
var sender = sender();
var player = player();
var engine = engine();
ta.set(Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, () ->
{
if (!player().getOpenInventory().getType().equals(InventoryType.CHEST)) {
if (!player.getOpenInventory().getType().equals(InventoryType.CHEST)) {
Bukkit.getScheduler().cancelTask(ta.get());
sender().sendMessage(C.GREEN + "Opened inventory!");
sender.sendMessage(C.GREEN + "Opened inventory!");
return;
}
@@ -241,13 +361,116 @@ public class CommandStudio implements DecreeExecutor {
inv.clear();
}
engine().addItems(true, inv, new RNG(RNG.r.imax()), tables, InventorySlotType.STORAGE, player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ(), 1);
engine.addItems(true, inv, new RNG(RNG.r.imax()), tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ(), 1);
}, 0, fast ? 5 : 35));
sender().sendMessage(C.GREEN + "Opening inventory now!");
player().openInventory(inv);
}
@Decree(description = "Calculate the chance for each region to generate", origin = DecreeOrigin.PLAYER)
public void regions(@Param(description = "The radius in chunks", defaultValue = "500") int radius) {
var engine = engine();
if (engine == null) {
sender().sendMessage(C.RED + "Only works in an Iris world!");
return;
}
var sender = sender();
var player = player();
Thread.ofVirtual()
.start(() -> {
int d = radius * 2;
KMap<String, AtomicInteger> data = new KMap<>();
engine.getDimension().getRegions().forEach(key -> data.put(key, new AtomicInteger(0)));
var multiBurst = new MultiBurst("Region Sampler");
var executor = multiBurst.burst(radius * radius);
sender.sendMessage(C.GRAY + "Generating data...");
var loc = player.getLocation();
int totalTasks = d * d;
AtomicInteger completedTasks = new AtomicInteger(0);
int c = J.ar(() -> sender.sendProgress((double) completedTasks.get() / totalTasks, "Finding regions"), 0);
new Spiraler(d, d, (x, z) -> executor.queue(() -> {
var region = engine.getRegion((x << 4) + 8, (z << 4) + 8);
data.computeIfAbsent(region.getLoadKey(), (k) -> new AtomicInteger(0))
.incrementAndGet();
completedTasks.incrementAndGet();
})).setOffset(loc.getBlockX(), loc.getBlockZ()).drain();
executor.complete();
multiBurst.close();
J.car(c);
sender.sendMessage(C.GREEN + "Done!");
var loader = engine.getData().getRegionLoader();
data.forEach((k, v) -> sender.sendMessage(C.GREEN + k + ": " + loader.load(k).getRarity() + " / " + Form.f((double) v.get() / totalTasks * 100, 2) + "%"));
});
}
@Decree(description = "Get all structures in a radius of chunks", aliases = "dist", origin = DecreeOrigin.PLAYER)
public void distances(@Param(description = "The radius in chunks") int radius) {
var engine = engine();
if (engine == null) {
sender().sendMessage(C.RED + "Only works in an Iris world!");
return;
}
var sender = sender();
int d = radius * 2;
KMap<String, KList<Position2>> data = new KMap<>();
var multiBurst = new MultiBurst("Distance Sampler");
var executor = multiBurst.burst(radius * radius);
sender.sendMessage(C.GRAY + "Generating data...");
var loc = player().getLocation();
int totalTasks = d * d;
AtomicInteger completedTasks = new AtomicInteger(0);
int c = J.ar(() -> {
sender.sendProgress((double) completedTasks.get() / totalTasks, "Finding structures");
}, 0);
new Spiraler(d, d, (x, z) -> executor.queue(() -> {
var struct = engine.getStructureAt(x, z);
if (struct != null) {
data.computeIfAbsent(struct.getLoadKey(), (k) -> new KList<>()).add(new Position2(x, z));
}
completedTasks.incrementAndGet();
})).setOffset(loc.getBlockX(), loc.getBlockZ()).drain();
executor.complete();
multiBurst.close();
J.car(c);
for (var key : data.keySet()) {
var list = data.get(key);
KList<Long> distances = new KList<>(list.size() - 1);
for (int i = 0; i < list.size(); i++) {
var pos = list.get(i);
double dist = Integer.MAX_VALUE;
for (var p : list) {
if (p.equals(pos)) continue;
dist = Math.min(dist, Math.sqrt(Math.pow(pos.getX() - p.getX(), 2) + Math.pow(pos.getZ() - p.getZ(), 2)));
}
if (dist == Integer.MAX_VALUE) continue;
distances.add(Math.round(dist * 16));
}
long[] array = new long[distances.size()];
for (int i = 0; i < distances.size(); i++) {
array[i] = distances.get(i);
}
Arrays.sort(array);
long min = array.length > 0 ? array[0] : 0;
long max = array.length > 0 ? array[array.length - 1] : 0;
long sum = Arrays.stream(array).sum();
long avg = array.length > 0 ? Math.round(sum / (double) array.length) : 0;
String msg = "%s: %s => min: %s/max: %s -> avg: %s".formatted(key, list.size(), min, max, avg);
sender.sendMessage(msg);
}
if (data.isEmpty()) {
sender.sendMessage(C.RED + "No data found!");
} else {
sender.sendMessage(C.GREEN + "Done!");
}
}
@Decree(description = "Render a world map (External GUI)", aliases = "render")
public void map(
@Param(name = "world", description = "The world to open the generator for", contextual = true)
@@ -260,7 +483,15 @@ public class CommandStudio implements DecreeExecutor {
return;
}
VisionGUI.launch(IrisToolbelt.access(world).getEngine(), 0);
Location center = sender().isPlayer() && player().getWorld().equals(world) ? player().getLocation() : null;
MapWindow.openMap(new PaletteExplorerStart(
IrisToolbelt.access(world).getEngine(),
world.getUID(),
sender().isPlayer() ? player().getUniqueId() : null,
null,
center == null ? 0D : center.getX(),
center == null ? 0D : center.getZ()
));
sender().sendMessage(C.GREEN + "Opening map!");
}
@@ -281,6 +512,7 @@ public class CommandStudio implements DecreeExecutor {
@Param(description = "The dimension to profile", contextual = true, defaultValue = "default")
IrisDimension dimension
) {
// Todo: Make this more accurate
File pack = dimension.getLoadFile().getParentFile().getParentFile();
File report = Iris.instance.getDataFile("profile.txt");
IrisProject project = new IrisProject(pack);
@@ -490,8 +722,14 @@ public class CommandStudio implements DecreeExecutor {
}
sender().sendMessage(C.GREEN + "Sending you to the studio world!");
player().teleport(Iris.service(StudioSVC.class).getActiveProject().getActiveProvider().getTarget().getWorld().spawnLocation());
player().setGameMode(GameMode.SPECTATOR);
var player = player();
player.teleportAsync(Iris.service(StudioSVC.class)
.getActiveProject()
.getActiveProvider()
.getTarget()
.getWorld()
.spawnLocation()
).thenRun(() -> player.setGameMode(GameMode.SPECTATOR));
}
@Decree(description = "Update your dimension projects VSCode workspace")
@@ -544,7 +782,6 @@ public class CommandStudio implements DecreeExecutor {
pw.println("Iris Version: " + Iris.instance.getDescription().getVersion());
pw.println("Bukkit Version: " + Bukkit.getBukkitVersion());
pw.println("MC Version: " + Bukkit.getVersion());
pw.println("PaperSpigot: " + (PaperLib.isPaper() ? "Yup!" : "Nope!"));
pw.println("Report Captured At: " + new Date());
pw.println("Chunks: (" + chunks.size() + "): ");
@@ -675,6 +912,10 @@ public class CommandStudio implements DecreeExecutor {
sender().sendMessage(C.RED + "You must have server launched GUIs enabled in the settings!");
return true;
}
if (!GuiHost.isAvailable()) {
sender().sendMessage(C.RED + "This server runs without a desktop, so it cannot open windows!");
return true;
}
return false;
}
@@ -0,0 +1,131 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.commands;
import com.volmit.iris.Iris;
import com.volmit.iris.core.pregenerator.LazyPregenerator;
import com.volmit.iris.core.pregenerator.TurboPregenerator;
import com.volmit.iris.core.pregenerator.TurboPregenerator;
import com.volmit.iris.util.decree.DecreeExecutor;
import com.volmit.iris.util.decree.annotations.Decree;
import com.volmit.iris.util.decree.annotations.Param;
import com.volmit.iris.util.format.C;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.util.Vector;
import java.io.File;
import java.io.IOException;
@Decree(name = "turbopregen", aliases = "turbo", description = "Pregenerate your Iris worlds!")
public class CommandTurboPregen implements DecreeExecutor {
public String worldName;
@Decree(description = "Pregenerate a world")
public void start(
@Param(description = "The radius of the pregen in blocks", aliases = "size")
int radius,
@Param(description = "The world to pregen", contextual = true)
World world,
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0")
Vector center
) {
worldName = world.getName();
File worldDirectory = world.getWorldFolder();
File TurboFile = new File(worldDirectory, "turbogen.json");
if (TurboFile.exists()) {
if (TurboPregenerator.getInstance() != null) {
sender().sendMessage(C.BLUE + "Turbo pregen is already in progress");
Iris.info(C.YELLOW + "Turbo pregen is already in progress");
return;
} else {
try {
TurboFile.delete();
} catch (Exception e){
Iris.error("Failed to delete the old instance file of Turbo Pregen!");
return;
}
}
}
try {
if (sender().isPlayer() && access() == null) {
sender().sendMessage(C.RED + "The engine access for this world is null!");
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
}
TurboPregenerator.TurboPregenJob pregenJob = TurboPregenerator.TurboPregenJob.builder()
.world(worldName)
.radiusBlocks(radius)
.position(0)
.build();
File TurboGenFile = new File(worldDirectory, "turbogen.json");
TurboPregenerator pregenerator = new TurboPregenerator(pregenJob, TurboGenFile);
pregenerator.start();
String msg = C.GREEN + "TurboPregen started in " + C.GOLD + worldName + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
sender().sendMessage(msg);
Iris.info(msg);
} catch (Throwable e) {
sender().sendMessage(C.RED + "Epic fail. See console.");
Iris.reportError(e);
e.printStackTrace();
}
}
@Decree(description = "Stop the active pregeneration task", aliases = "x")
public void stop(@Param(aliases = "world", description = "The world to pause") World world) throws IOException {
TurboPregenerator turboPregenInstance = TurboPregenerator.getInstance();
File worldDirectory = world.getWorldFolder();
File turboFile = new File(worldDirectory, "turbogen.json");
if (turboPregenInstance != null) {
turboPregenInstance.shutdownInstance(world);
sender().sendMessage(C.LIGHT_PURPLE + "Closed Turbogen instance for " + world.getName());
} else if (turboFile.exists() && turboFile.delete()) {
sender().sendMessage(C.LIGHT_PURPLE + "Closed Turbogen instance for " + world.getName());
} else if (turboFile.exists()) {
Iris.error("Failed to delete the old instance file of Turbo Pregen!");
} else {
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
}
}
@Decree(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"})
public void pause(
@Param(aliases = "world", description = "The world to pause")
World world
) {
if (TurboPregenerator.getInstance() != null) {
TurboPregenerator.setPausedTurbo(world);
sender().sendMessage(C.GREEN + "Paused/unpaused Turbo Pregen, now: " + (TurboPregenerator.isPausedTurbo(world) ? "Paused" : "Running") + ".");
} else {
File worldDirectory = world.getWorldFolder();
File TurboFile = new File(worldDirectory, "turbogen.json");
if (TurboFile.exists()){
TurboPregenerator.loadTurboGenerator(world.getName());
sender().sendMessage(C.YELLOW + "Started Turbo Pregen back up!");
} else {
sender().sendMessage(C.YELLOW + "No active Turbo Pregen tasks to pause/unpause.");
}
}
}
}
@@ -0,0 +1,102 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.commands;
import lombok.Synchronized;
import org.bukkit.World;
import com.volmit.iris.Iris;
import com.volmit.iris.core.pregenerator.ChunkUpdater;
import com.volmit.iris.core.tools.IrisToolbelt;
import com.volmit.iris.util.decree.DecreeExecutor;
import com.volmit.iris.util.decree.DecreeOrigin;
import com.volmit.iris.util.decree.annotations.Decree;
import com.volmit.iris.util.decree.annotations.Param;
import com.volmit.iris.util.format.C;
import com.volmit.iris.util.format.Form;
@Decree(name = "updater", origin = DecreeOrigin.BOTH, description = "Iris World Updater")
public class CommandUpdater implements DecreeExecutor {
private final Object lock = new Object();
private transient ChunkUpdater chunkUpdater;
@Decree(description = "Updates all chunk in the specified world")
public void start(
@Param(description = "World to update chunks at", contextual = true)
World world
) {
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(C.GOLD + "This is not an Iris world");
return;
}
synchronized (lock) {
if (chunkUpdater != null) {
chunkUpdater.stop();
}
chunkUpdater = new ChunkUpdater(world);
if (sender().isPlayer()) {
sender().sendMessage(C.GREEN + "Updating " + world.getName() + C.GRAY + " Total chunks: " + Form.f(chunkUpdater.getChunks()));
} else {
Iris.info(C.GREEN + "Updating " + world.getName() + C.GRAY + " Total chunks: " + Form.f(chunkUpdater.getChunks()));
}
chunkUpdater.start();
}
}
@Synchronized("lock")
@Decree(description = "Pause the updater")
public void pause( ) {
if (chunkUpdater == null) {
sender().sendMessage(C.GOLD + "You cant pause something that doesnt exist?");
return;
}
boolean status = chunkUpdater.pause();
if (sender().isPlayer()) {
if (status) {
sender().sendMessage(C.IRIS + "Paused task for: " + C.GRAY + chunkUpdater.getName());
} else {
sender().sendMessage(C.IRIS + "Unpause task for: " + C.GRAY + chunkUpdater.getName());
}
} else {
if (status) {
Iris.info(C.IRIS + "Paused task for: " + C.GRAY + chunkUpdater.getName());
} else {
Iris.info(C.IRIS + "Unpause task for: " + C.GRAY + chunkUpdater.getName());
}
}
}
@Synchronized("lock")
@Decree(description = "Stops the updater")
public void stop() {
if (chunkUpdater == null) {
sender().sendMessage(C.GOLD + "You cant stop something that doesnt exist?");
return;
}
if (sender().isPlayer()) {
sender().sendMessage("Stopping Updater for: " + C.GRAY + chunkUpdater.getName());
} else {
Iris.info("Stopping Updater for: " + C.GRAY + chunkUpdater.getName());
}
chunkUpdater.stop();
}
}
@@ -22,7 +22,9 @@ import com.volmit.iris.Iris;
import com.volmit.iris.core.edit.BlockSignal;
import com.volmit.iris.core.nms.INMS;
import com.volmit.iris.core.tools.IrisToolbelt;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.engine.object.IrisRegion;
import com.volmit.iris.util.data.B;
import com.volmit.iris.util.decree.DecreeExecutor;
import com.volmit.iris.util.decree.DecreeOrigin;
@@ -37,6 +39,7 @@ import org.bukkit.Material;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
@Decree(name = "what", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris What?")
@@ -82,6 +85,19 @@ public class CommandWhat implements DecreeExecutor {
}
}
@Decree(description = "What region am i in?", origin = DecreeOrigin.PLAYER)
public void region() {
try {
Chunk chunk = world().getChunkAt(player().getLocation().getBlockZ() / 16, player().getLocation().getBlockZ() / 16);
IrisRegion r = engine().getRegion(chunk);
sender().sendMessage("IRegion: " + r.getLoadKey() + " (" + r.getName() + ")");
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(C.IRIS + "Iris worlds only.");
}
}
@Decree(description = "What block am i looking at?", origin = DecreeOrigin.PLAYER)
public void block() {
BlockData bd;
@@ -147,7 +163,7 @@ public class CommandWhat implements DecreeExecutor {
sender().sendMessage("Found " + v.get() + " Nearby Markers (" + marker + ")");
} else {
sender().sendMessage("Iris worlds only.");
sender().sendMessage(C.IRIS + "Iris worlds only.");
}
}
}
@@ -61,14 +61,14 @@ public class JigsawEditor implements Listener {
private Location target;
public JigsawEditor(Player player, IrisJigsawPiece piece, IrisObject object, File saveLocation) {
if (editors.containsKey(player)) {
editors.get(player).close();
}
if (object == null) throw new RuntimeException("Object is null! " + piece.getObject());
editors.compute(player, ($, current) -> {
if (current != null) {
current.exit();
}
return this;
});
editors.put(player, this);
if (object == null) {
throw new RuntimeException("Object is null! " + piece.getObject());
}
this.object = object;
this.player = player;
origin = player.getLocation().clone().add(0, 7, 0);
@@ -0,0 +1,113 @@
package com.volmit.iris.core.events;
import com.volmit.iris.Iris;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.InventorySlotType;
import com.volmit.iris.engine.object.IrisLootTable;
import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.scheduling.J;
import lombok.Getter;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.bukkit.event.world.LootGenerateEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.bukkit.loot.LootContext;
import org.bukkit.loot.LootTable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.Random;
@Getter
public class IrisLootEvent extends Event {
private static final HandlerList handlers = new HandlerList();
private static final LootTable EMPTY = new LootTable() {
@NotNull
@Override
public NamespacedKey getKey() {
return new NamespacedKey(Iris.instance, "empty");
}
@NotNull
@Override
public Collection<ItemStack> populateLoot(@Nullable Random random, @NotNull LootContext context) {
return List.of();
}
@Override
public void fillInventory(@NotNull Inventory inventory, @Nullable Random random, @NotNull LootContext context) {
}
};
private final Engine engine;
private final Block block;
private final InventorySlotType slot;
private final KList<IrisLootTable> tables;
/**
* Constructor for IrisLootEvent with mode selection.
*
* @param engine The engine instance.
* @param block The block associated with the event.
* @param slot The inventory slot type.
* @param tables The list of IrisLootTables. (mutable*)
*/
public IrisLootEvent(Engine engine, Block block, InventorySlotType slot, KList<IrisLootTable> tables) {
this.engine = engine;
this.block = block;
this.slot = slot;
this.tables = tables;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
/**
* Required method to get the HandlerList for this event.
*
* @return The HandlerList.
*/
public static HandlerList getHandlerList() {
return handlers;
}
/**
* Triggers the corresponding Bukkit loot event.
* This method integrates your custom IrisLootTables with Bukkit's LootGenerateEvent,
* allowing other plugins to modify or cancel the loot generation.
*
* @return true when the event was canceled
*/
public static boolean callLootEvent(KList<ItemStack> loot, Inventory inv, World world, int x, int y, int z) {
InventoryHolder holder = inv.getHolder();
Location loc = new Location(world, x, y, z);
if (holder == null) {
holder = new InventoryHolder() {
@NotNull
@Override
public Inventory getInventory() {
return inv;
}
};
}
LootContext context = new LootContext.Builder(loc).build();
LootGenerateEvent event = new LootGenerateEvent(world, null, holder, EMPTY, context, loot, true);
if (!Bukkit.isPrimaryThread()) {
Iris.warn("LootGenerateEvent was not called on the main thread, please report this issue.");
Thread.dumpStack();
J.sfut(() -> Bukkit.getPluginManager().callEvent(event)).join();
} else Bukkit.getPluginManager().callEvent(event);
return event.isCancelled();
}
}
@@ -0,0 +1,96 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import java.util.Random;
// A copy of Minecraft's Biome.BIOME_INFO_NOISE, the 2D simplex noise swamps use to pick between their two grass colours.
// Minecraft seeds it through a LegacyRandomSource, which draws the same numbers as java.util.Random.
final class BiomeInfoNoise {
private static final long SEED = 2345L;
private static final int PERMUTATION_SIZE = 256;
private static final int GRADIENT_COUNT = 12;
private static final double SKEW = 0.5D * (Math.sqrt(3D) - 1D);
private static final double UNSKEW = (3D - Math.sqrt(3D)) / 6D;
private static final double CORNER_RADIUS = 0.5D;
private static final double OUTPUT_SCALE = 70D;
private static final int[][] GRADIENTS = {
{1, 1}, {-1, 1}, {1, -1}, {-1, -1},
{1, 0}, {-1, 0}, {1, 0}, {-1, 0},
{0, 1}, {0, -1}, {0, 1}, {0, -1}
};
private static final int[] PERMUTATION = createPermutation();
private BiomeInfoNoise() {
}
static float at(double x, double z) {
double skewOffset = (x + z) * SKEW;
int cellX = (int) Math.floor(x + skewOffset);
int cellZ = (int) Math.floor(z + skewOffset);
double unskewOffset = (cellX + cellZ) * UNSKEW;
double x0 = x - (cellX - unskewOffset);
double z0 = z - (cellZ - unskewOffset);
int stepX = x0 > z0 ? 1 : 0;
int stepZ = 1 - stepX;
double x1 = x0 - stepX + UNSKEW;
double z1 = z0 - stepZ + UNSKEW;
double x2 = x0 - 1D + 2D * UNSKEW;
double z2 = z0 - 1D + 2D * UNSKEW;
int wrappedX = cellX & 0xFF;
int wrappedZ = cellZ & 0xFF;
int gradient0 = permute(wrappedX + permute(wrappedZ)) % GRADIENT_COUNT;
int gradient1 = permute(wrappedX + stepX + permute(wrappedZ + stepZ)) % GRADIENT_COUNT;
int gradient2 = permute(wrappedX + 1 + permute(wrappedZ + 1)) % GRADIENT_COUNT;
return (float) (OUTPUT_SCALE * (corner(gradient0, x0, z0) + corner(gradient1, x1, z1) + corner(gradient2, x2, z2)));
}
private static double corner(int gradient, double x, double z) {
double falloff = CORNER_RADIUS - x * x - z * z;
if (falloff < 0D) {
return 0D;
}
falloff *= falloff;
return falloff * falloff * (GRADIENTS[gradient][0] * x + GRADIENTS[gradient][1] * z);
}
private static int permute(int index) {
return PERMUTATION[index & 0xFF];
}
// Minecraft draws three offsets first and discards them for this noise, so they are drawn here too to stay in step.
private static int[] createPermutation() {
Random random = new Random(SEED);
for (int discardedOffset = 0; discardedOffset < 3; discardedOffset++) {
random.nextDouble();
}
int[] permutation = new int[PERMUTATION_SIZE];
for (int i = 0; i < PERMUTATION_SIZE; i++) {
permutation[i] = i;
}
for (int i = 0; i < PERMUTATION_SIZE; i++) {
int swap = i + random.nextInt(PERMUTATION_SIZE - i);
int previous = permutation[i];
permutation[i] = permutation[swap];
permutation[swap] = previous;
}
return permutation;
}
}
@@ -0,0 +1,195 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.engine.object.IrisBiomePaletteLayer;
import com.volmit.iris.engine.object.IrisBlockData;
import com.volmit.iris.engine.object.IrisDecorator;
import com.volmit.iris.util.collection.KList;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* One of the block palettes a biome places: a surface, sea or cave ceiling layer, its wall or slab blocks, or a
* decorator.
*/
record BiomePalette(String biomeKey, Kind kind, int index) {
private static final String ID_PREFIX = "palette:";
private static final char ID_SEPARATOR = '#';
private static final String SNIPPET_PREFIX = "snippet/";
enum Kind {
LAYER("Layer", "layers", SnippetFolder.BLOCK_PALETTES, true, true),
SEA_LAYER("Sea layer", "seaLayers", SnippetFolder.BLOCK_PALETTES, true, true),
CAVE_CEILING_LAYER("Cave ceiling layer", "caveCeilingLayers", SnippetFolder.BLOCK_PALETTES, true, true),
WALL("Wall", "wall", SnippetFolder.BLOCK_PALETTES, false, false),
SLAB("Slab", "slab", SnippetFolder.BLOCK_PALETTES, false, false),
DECORATOR("Decorator", "decorators", SnippetFolder.DECORATORS, false, true);
private final String label;
private final String jsonKey;
private final SnippetFolder snippetFolder;
private final boolean stacked;
private final boolean numbered;
Kind(String label, String jsonKey, SnippetFolder snippetFolder, boolean stacked, boolean numbered) {
this.label = label;
this.jsonKey = jsonKey;
this.snippetFolder = snippetFolder;
this.stacked = stacked;
this.numbered = numbered;
}
SnippetFolder snippetFolder() {
return snippetFolder;
}
/**
* Whether the palette is a stack of blocks with a thickness, rather than single blocks on walls or slopes.
*/
boolean isStacked() {
return stacked;
}
/**
* The biome file's key holding these palettes: a list of them, or a single one for walls and slabs.
*/
String jsonKey() {
return jsonKey;
}
// A biome without wall or slab blocks still has those palettes, just empty.
List<IrisBiomePaletteLayer> layersOf(IrisBiome biome) {
return switch (this) {
case LAYER -> biome.getLayers();
case SEA_LAYER -> biome.getSeaLayers();
case CAVE_CEILING_LAYER -> biome.getCaveCeilingLayers();
case WALL -> biome.getWall().getPalette().isEmpty() ? List.of() : List.of(biome.getWall());
case SLAB -> biome.getSlab().getPalette().isEmpty() ? List.of() : List.of(biome.getSlab());
case DECORATOR -> List.of();
};
}
// A decorator snippet that couldn't be found loads as an empty entry, which places nothing.
List<KList<IrisBlockData>> blockListsOf(IrisBiome biome) {
if (this != DECORATOR) {
return layersOf(biome).stream().map(IrisBiomePaletteLayer::getPalette).toList();
}
return biome.getDecorators().stream()
.map(decorator -> decorator == null ? new KList<IrisBlockData>() : decorator.getPalette())
.toList();
}
}
/**
* The folders under snippet/ that palettes can come from, named after the Iris type they hold.
*/
enum SnippetFolder {
BLOCK_PALETTES("biome-palette", "Block palettes"),
DECORATORS("decorator", "Decorators");
private final String folder;
private final String label;
SnippetFolder(String folder, String label) {
this.folder = folder;
this.label = label;
}
String folder() {
return folder;
}
String label() {
return label;
}
/**
* The snippet's path inside this folder. Packs may leave the folder out, the way the snippet loader allows.
*/
String pathOf(String reference) {
String path = reference.startsWith(SNIPPET_PREFIX) ? reference.substring(SNIPPET_PREFIX.length()) : reference;
return path.startsWith(folder + "/") ? path.substring(folder.length() + 1) : path;
}
}
static List<BiomePalette> allOf(String biomeKey, IrisBiome biome) {
List<BiomePalette> palettes = new ArrayList<>();
for (Kind kind : Kind.values()) {
List<KList<IrisBlockData>> blockLists = kind.blockListsOf(biome);
for (int index = 0; index < blockLists.size(); index++) {
if (!blockLists.get(index).isEmpty()) {
palettes.add(new BiomePalette(biomeKey, kind, index));
}
}
}
return List.copyOf(palettes);
}
static BiomePalette fromId(String id) {
if (id == null || !id.startsWith(ID_PREFIX)) {
return null;
}
int indexSeparator = id.lastIndexOf(ID_SEPARATOR);
int kindSeparator = id.lastIndexOf(ID_SEPARATOR, indexSeparator - 1);
if (kindSeparator < ID_PREFIX.length()) {
return null;
}
return new BiomePalette(
id.substring(ID_PREFIX.length(), kindSeparator),
Kind.valueOf(id.substring(kindSeparator + 1, indexSeparator)),
Integer.parseInt(id.substring(indexSeparator + 1))
);
}
static boolean belongsTo(String id, String biomeKey) {
return id != null && id.startsWith(ID_PREFIX + biomeKey + ID_SEPARATOR);
}
String id() {
return ID_PREFIX + biomeKey + ID_SEPARATOR + kind.name() + ID_SEPARATOR + index;
}
String label() {
return kind.numbered ? kind.label + " " + index : kind.label;
}
String lowercaseLabel() {
return label().toLowerCase(Locale.ROOT);
}
/**
* The palette's blocks in the given copy of the biome, or null when a hotload removed it.
*/
KList<IrisBlockData> blocksIn(IrisBiome biome) {
List<KList<IrisBlockData>> blockLists = kind.blockListsOf(biome);
return index < blockLists.size() ? blockLists.get(index) : null;
}
IrisBiomePaletteLayer layerIn(IrisBiome biome) {
return kind.layersOf(biome).get(index);
}
IrisDecorator decoratorIn(IrisBiome biome) {
return biome.getDecorators().get(index);
}
}
@@ -0,0 +1,325 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.engine.decorator.IrisEngineDecorator;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.engine.object.IrisBiomePaletteLayer;
import com.volmit.iris.engine.object.IrisBlockData;
import com.volmit.iris.engine.object.IrisDecorationPart;
import com.volmit.iris.engine.object.IrisDecorator;
import com.volmit.iris.engine.object.IrisDimension;
import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.math.RNG;
import com.volmit.iris.util.noise.CNG;
import org.bukkit.block.data.Bisected;
import java.util.ArrayList;
import java.util.List;
/**
* How thick one biome palette is at a spot and which noise picks its block there, sampled exactly the way the
* generator does for that kind of palette. The generator seeds each palette's noise from the first call it makes,
* so these use the same random sources the terrain, carving, post and decorator passes use.
*/
abstract class BiomePalettePlacement {
protected final IrisData data;
private BiomePalettePlacement(IrisData data) {
this.data = data;
}
static BiomePalettePlacement of(Engine engine, IrisBiome biome, BiomePalette palette) {
IrisData data = engine.getData();
RNG terrainRandom = new RNG(engine.getSeedManager().getTerrain());
RNG carveRandom = new RNG(engine.getSeedManager().getCarve());
RNG postRandom = new RNG(engine.getSeedManager().getPost());
int seaLevel = seaLevel(engine.getDimension());
return switch (palette.kind()) {
case LAYER -> new SurfaceLayer(biome, palette.index(), palette.layerIn(biome), terrainRandom, data);
case SEA_LAYER -> new LegacyStackedLayer(palette.layerIn(biome), data, terrainRandom.nextParallelRNG(palette.index()),
biome.getLayerSeaHeightGenerators(terrainRandom, data), palette.index());
case CAVE_CEILING_LAYER -> new LegacyStackedLayer(palette.layerIn(biome), data, carveRandom.nextParallelRNG(palette.index()),
biome.getLayerHeightGenerators(terrainRandom, data), palette.index());
case WALL -> new SingleBlocks(palette.layerIn(biome), data, carveRandom, seaLevel);
case SLAB -> new SingleBlocks(palette.layerIn(biome), data, postRandom, seaLevel);
case DECORATOR -> new Decorations(biome, palette.decoratorIn(biome), engine.getSeedManager().getComponent(),
seaLevel, data);
};
}
private static int seaLevel(IrisDimension dimension) {
return dimension == null ? 0 : dimension.getFluidHeight();
}
abstract KList<IrisBlockData> blocks();
abstract double zoom();
int minThickness() {
return 1;
}
int maxThickness() {
return 1;
}
abstract boolean hasSlopeCondition();
/**
* How many blocks of this palette are stacked at the spot.
*/
abstract int thickness(int blockX, int blockZ);
abstract double noise(int blockX, int blockZ, int depth);
/**
* What the preview simplifies about this palette, or null when it shows exactly what the world places.
*/
String simplification() {
return null;
}
private abstract static class LayerPlacement extends BiomePalettePlacement {
protected final IrisBiomePaletteLayer layer;
private LayerPlacement(IrisBiomePaletteLayer layer, IrisData data) {
super(data);
this.layer = layer;
}
@Override
KList<IrisBlockData> blocks() {
return layer.getPalette();
}
@Override
double zoom() {
return layer.getZoom();
}
@Override
int minThickness() {
return layer.getMinHeight();
}
@Override
int maxThickness() {
return layer.getMaxHeight();
}
@Override
boolean hasSlopeCondition() {
return !layer.getSlopeCondition().isDefault();
}
}
private static final class SurfaceLayer extends LayerPlacement {
private final IrisBiome biome;
private final int index;
private final RNG random;
private SurfaceLayer(IrisBiome biome, int index, IrisBiomePaletteLayer layer, RNG random, IrisData data) {
super(layer, data);
this.biome = biome;
this.index = index;
this.random = random;
}
@Override
int thickness(int blockX, int blockZ) {
return biome.getLayerThickness(index, blockX, blockZ, random, data);
}
@Override
double noise(int blockX, int blockZ, int depth) {
return biome.getLayerNoise(index, blockX, blockZ, depth, random, data);
}
}
/**
* Sea and cave ceiling layers go through the older sampling path, which divides by the zoom twice and borrows
* its thickness noise from another list: a cave ceiling layer uses the surface layer at the same position's.
*/
private static final class LegacyStackedLayer extends LayerPlacement {
private final RNG random;
private final CNG thicknessNoise;
private LegacyStackedLayer(IrisBiomePaletteLayer layer, IrisData data, RNG random, KList<CNG> thicknessNoises,
int index) {
super(layer, data);
this.random = random;
this.thicknessNoise = index < thicknessNoises.size() ? thicknessNoises.get(index) : null;
}
@Override
int thickness(int blockX, int blockZ) {
if (thicknessNoise == null) {
return 0;
}
double zoom = layer.getZoom();
return Math.max(0, thicknessNoise.fit(layer.getMinHeight(), layer.getMaxHeight(), blockX / zoom, blockZ / zoom));
}
@Override
double noise(int blockX, int blockZ, int depth) {
double zoom = layer.getZoom();
return layer.getLayerGenerator(random, data)
.noise((blockX + depth) / zoom / zoom, depth / zoom, (blockZ - depth) / zoom / zoom);
}
@Override
String simplification() {
return thicknessNoise == null
? "The generator takes this layer's thickness from the surface layer at the same position, which doesn't exist, so it is never placed."
: null;
}
}
/**
* Wall and slab blocks are picked one at a time with noise that also changes with height.
*/
private static final class SingleBlocks extends LayerPlacement {
private final RNG random;
private final int sampleHeight;
private SingleBlocks(IrisBiomePaletteLayer layer, IrisData data, RNG random, int sampleHeight) {
super(layer, data);
this.random = random;
this.sampleHeight = sampleHeight;
}
@Override
int minThickness() {
return 1;
}
@Override
int maxThickness() {
return 1;
}
@Override
int thickness(int blockX, int blockZ) {
return 1;
}
@Override
double noise(int blockX, int blockZ, int depth) {
double zoom = layer.getZoom();
return layer.getLayerGenerator(random, data).noise(blockX / zoom, sampleHeight / zoom, blockZ / zoom);
}
@Override
String simplification() {
return "These blocks also change with height; the preview shows them at sea level (Y " + sampleHeight + ").";
}
}
/**
* A decorator places where its style noise stays under its chance, and its variance noise picks the block there.
* The generator seeds the variance noise from whichever block it decorates first, so the preview reuses that noise
* once the world has generated some decorations.
*/
private static final class Decorations extends BiomePalettePlacement {
private final IrisDecorator decorator;
private final CNG placementNoise;
private final CNG blockNoise;
private final int sampleHeight;
private final boolean sharesSpotsWithOtherDecorators;
private Decorations(IrisBiome biome, IrisDecorator decorator, long componentSeed, int sampleHeight, IrisData data) {
super(data);
this.decorator = decorator;
RNG partRandom = new RNG(IrisEngineDecorator.partSeed(componentSeed, decorator.getPartOf()));
this.placementNoise = decorator.getGenerator(partRandom, data);
this.blockNoise = decorator.getVarianceGenerator(partRandom, data);
this.sampleHeight = sampleHeight;
this.sharesSpotsWithOtherDecorators = biome.getDecorators().stream()
.anyMatch(other -> other != null && other != decorator && other.getPartOf() == decorator.getPartOf());
}
@Override
KList<IrisBlockData> blocks() {
return decorator.getPalette();
}
@Override
double zoom() {
return decorator.getStyle().getZoom();
}
@Override
boolean hasSlopeCondition() {
return !decorator.getSlopeCondition().isDefault();
}
@Override
int thickness(int blockX, int blockZ) {
double zoom = zoom();
return placementNoise.fitDouble(0D, 1D, blockX / zoom, blockZ / zoom) <= decorator.getChance() ? 1 : 0;
}
// The generator swaps X and Z when it picks the block.
@Override
double noise(int blockX, int blockZ, int depth) {
return blockNoise.noise(blockZ, sampleHeight, blockX);
}
@Override
String simplification() {
List<String> notes = new ArrayList<>();
String partNote = partNote(decorator.getPartOf());
if (partNote != null) {
notes.add(partNote);
}
if (sharesSpotsWithOtherDecorators) {
notes.add("Where other decorators of this biome could place too, the world picks one of them at random.");
}
if (placesTwoBlockTallPlants()) {
notes.add("Two block tall plants stand up on the map: their top half covers the block above them.");
}
if (decorator.isStacking()) {
notes.add("It stacks " + decorator.getStackMin() + "–" + decorator.getStackMax()
+ " blocks high; the preview shows the bottom block.");
}
if (blocks().size() > 1) {
notes.add("The block also changes with height; the preview picks it at sea level (Y " + sampleHeight + ").");
}
return notes.isEmpty() ? null : String.join(" ", notes);
}
// The generator places any two-part block as a bottom half with its top half above it.
private boolean placesTwoBlockTallPlants() {
return !decorator.isStacking() && blocks().stream().anyMatch(block -> block.getBlockData(data) instanceof Bisected);
}
private static String partNote(IrisDecorationPart part) {
return switch (part) {
case NONE -> null;
case SHORE_LINE -> "It only places along shore lines.";
case SEA_SURFACE -> "It only places on the sea surface.";
case SEA_FLOOR -> "It only places on the sea floor.";
case CEILING -> "It places on cave ceilings and the undersides of overhangs, not on the ground.";
};
}
}
}
@@ -16,10 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.util.reflect;
public class ShadeFix {
public static void fix(Class<?> c) {
c.getCanonicalName();
}
package com.volmit.iris.core.gui;
enum BiomeTint {
GRASS,
FOLIAGE,
DRY_FOLIAGE,
WATER
}
@@ -0,0 +1,68 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import java.util.Locale;
// The colours one Minecraft biome multiplies onto tinted blocks, as worked out by Biome#getGrassColor and friends.
record BiomeTintColors(int grass, GrassColorModifier grassModifier, int foliage, int dryFoliage, int water) {
int colorAt(BiomeTint tint, int blockX, int blockZ) {
return switch (tint) {
case GRASS -> grassModifier.modify(blockX, blockZ, grass);
case FOLIAGE -> foliage;
case DRY_FOLIAGE -> dryFoliage;
case WATER -> water;
};
}
boolean variesByPosition() {
return grassModifier == GrassColorModifier.SWAMP;
}
enum GrassColorModifier {
NONE,
DARK_FOREST,
SWAMP;
private static final double SWAMP_NOISE_SCALE = 0.0225D;
private static final double SWAMP_WET_THRESHOLD = -0.1D;
private static final int SWAMP_WET_GRASS = 0x4C763C;
private static final int SWAMP_DRY_GRASS = 0x6A7039;
private static final int DARK_FOREST_SHIFT = 0x28340A;
static GrassColorModifier named(String name) {
return switch (name.toLowerCase(Locale.ROOT)) {
case "dark_forest" -> DARK_FOREST;
case "swamp" -> SWAMP;
default -> NONE;
};
}
int modify(int blockX, int blockZ, int baseColor) {
return switch (this) {
case NONE -> baseColor;
case DARK_FOREST -> ((baseColor & 0xFEFEFE) + DARK_FOREST_SHIFT) >> 1;
case SWAMP -> BiomeInfoNoise.at(blockX * SWAMP_NOISE_SCALE, blockZ * SWAMP_NOISE_SCALE) < SWAMP_WET_THRESHOLD
? SWAMP_WET_GRASS
: SWAMP_DRY_GRASS;
};
}
}
}
@@ -0,0 +1,138 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.engine.object.IrisBiomeCustom;
import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.math.RNG;
import org.bukkit.block.Biome;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
// The tint Minecraft draws on each column of one Iris biome. Iris picks the Minecraft biome once per chunk
// (see IrisBiomeActuator), and the client averages the colours of the columns around each block.
final class BiomeTintField {
private static final int MINECRAFT_DEFAULT_BLEND_RADIUS = 2;
private static final int BLENDED_COLUMN_COUNT = (MINECRAFT_DEFAULT_BLEND_RADIUS * 2 + 1) * (MINECRAFT_DEFAULT_BLEND_RADIUS * 2 + 1);
private static final int BLENDED_TINT_CACHE_SIZE = 1 << 16;
private static final int CHUNK_COLORS_CACHE_SIZE = 1 << 10;
private final IrisBiome biome;
private final RNG biomeRandom;
private final MinecraftBiomeColors minecraftColors;
private final BiomeTintColors sameColorsEverywhere;
private final Map<IrisBiomeCustom, BiomeTintColors> colorsByCustomBiome = new ConcurrentHashMap<>();
private final BlendedTint[] blendedTintCache = new BlendedTint[BLENDED_TINT_CACHE_SIZE];
private final ChunkColors[] chunkColorsCache = new ChunkColors[CHUNK_COLORS_CACHE_SIZE];
BiomeTintField(IrisBiome biome, RNG biomeRandom, MinecraftBiomeColors minecraftColors) {
this.biome = biome;
this.biomeRandom = biomeRandom;
this.minecraftColors = minecraftColors;
BiomeTintColors onlyColors = onlyPossibleColors();
this.sameColorsEverywhere = onlyColors == null || onlyColors.variesByPosition() ? null : onlyColors;
}
int colorAt(BiomeTint tint, int blockX, int blockZ) {
if (sameColorsEverywhere != null) {
return sameColorsEverywhere.colorAt(tint, blockX, blockZ);
}
int cacheSlot = cacheSlot(blockX, blockZ, tint.ordinal(), BLENDED_TINT_CACHE_SIZE);
BlendedTint cached = blendedTintCache[cacheSlot];
if (cached != null && cached.x() == blockX && cached.z() == blockZ && cached.tint() == tint) {
return cached.color();
}
int color = blendedColor(tint, blockX, blockZ);
blendedTintCache[cacheSlot] = new BlendedTint(blockX, blockZ, tint, color);
return color;
}
// Minecraft's ClientLevel#calculateBlockTint at the default biome blend radius.
private int blendedColor(BiomeTint tint, int blockX, int blockZ) {
int red = 0;
int green = 0;
int blue = 0;
for (int x = blockX - MINECRAFT_DEFAULT_BLEND_RADIUS; x <= blockX + MINECRAFT_DEFAULT_BLEND_RADIUS; x++) {
for (int z = blockZ - MINECRAFT_DEFAULT_BLEND_RADIUS; z <= blockZ + MINECRAFT_DEFAULT_BLEND_RADIUS; z++) {
int color = chunkColors(x >> 4, z >> 4).colorAt(tint, x, z);
red += (color >>> 16) & 0xFF;
green += (color >>> 8) & 0xFF;
blue += color & 0xFF;
}
}
return (red / BLENDED_COLUMN_COUNT) << 16 | (green / BLENDED_COLUMN_COUNT) << 8 | (blue / BLENDED_COLUMN_COUNT);
}
private BiomeTintColors chunkColors(int chunkX, int chunkZ) {
int cacheSlot = cacheSlot(chunkX, chunkZ, 0, CHUNK_COLORS_CACHE_SIZE);
ChunkColors cached = chunkColorsCache[cacheSlot];
if (cached != null && cached.chunkX() == chunkX && cached.chunkZ() == chunkZ) {
return cached.colors();
}
BiomeTintColors colors = colorsForChunkAt(chunkX << 4, chunkZ << 4);
chunkColorsCache[cacheSlot] = new ChunkColors(chunkX, chunkZ, colors);
return colors;
}
private BiomeTintColors colorsForChunkAt(int originX, int originZ) {
if (biome.isCustom()) {
return customColors(biome.getCustomBiome(biomeRandom, originX, 0, originZ));
}
return vanillaColors(biome.getSkyBiome(biomeRandom, originX, 0, originZ));
}
// Follows the same choices as IrisBiome#getSkyBiome and #getCustomBiome, returning null when chunks can differ.
private BiomeTintColors onlyPossibleColors() {
if (biome.isCustom()) {
KList<IrisBiomeCustom> variants = biome.getCustomDerivitives();
return variants.size() == 1 ? customColors(variants.get(0)) : null;
}
KList<Biome> skyBiomes = biome.getBiomeSkyScatter();
if (!skyBiomes.isEmpty()) {
return skyBiomes.size() == 1 ? vanillaColors(skyBiomes.get(0)) : null;
}
KList<Biome> groundBiomes = biome.getBiomeScatter();
if (groundBiomes.isEmpty()) {
return vanillaColors(biome.getDerivative());
}
return groundBiomes.size() == 1 ? vanillaColors(groundBiomes.get(0)) : null;
}
private BiomeTintColors customColors(IrisBiomeCustom customBiome) {
return colorsByCustomBiome.computeIfAbsent(customBiome, minecraftColors::custom);
}
private BiomeTintColors vanillaColors(Biome vanillaBiome) {
return minecraftColors.vanilla(vanillaBiome.getKey());
}
private static int cacheSlot(int x, int z, int salt, int cacheSize) {
long hash = x * 0x9E3779B97F4A7C15L ^ z * 0xC2B2AE3D27D4EB4FL ^ salt * 0x165667B19E3779F9L;
return (int) ((hash ^ (hash >>> 29)) & (cacheSize - 1));
}
private record BlendedTint(int x, int z, BiomeTint tint, int color) {
}
private record ChunkColors(int chunkX, int chunkZ, BiomeTintColors colors) {
}
}
@@ -0,0 +1,213 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.UnaryOperator;
/**
* A block as the object viewer draws it: the textured faces of its model, in block units, already turned the way the
* blockstate faces.
*
* @param faces every face of every box in the model
* @param coveredSides the block's sides that a solid face fills completely, which hide whatever touches them
* @param hidesSameBlockSides whether the model fills the whole block, so like glass it hides the sides between two of
* the same blocks
*/
record BlockModel(List<Face> faces, Set<CubeFace> coveredSides, boolean hidesSameBlockSides) {
static final BlockModel EMPTY = new BlockModel(List.of(), Set.of(), false);
private static final int SIDE_CELLS = 16;
private static final double ON_SIDE_TOLERANCE = 1e-4D;
static BlockModel of(List<Face> faces) {
Set<CubeFace> coveredSides = EnumSet.noneOf(CubeFace.class);
boolean fillsEverySide = true;
for (CubeFace side : CubeFace.values()) {
if (sideIsFilled(faces, side, true)) {
coveredSides.add(side);
}
fillsEverySide &= sideIsFilled(faces, side, false);
}
return new BlockModel(List.copyOf(faces), Set.copyOf(coveredSides), fillsEverySide);
}
static BlockModel solidColor(int color) {
BlockTexture texture = BlockTexture.solid(color);
List<Face> faces = new ArrayList<>();
for (CubeFace side : CubeFace.values()) {
faces.add(Face.boxSide(Vec3.ZERO, new Vec3(1D, 1D, 1D), side, texture, side));
}
return of(faces);
}
boolean isEmpty() {
return faces.isEmpty();
}
BlockModel withTextures(UnaryOperator<BlockTexture> change) {
Map<BlockTexture, BlockTexture> changed = new IdentityHashMap<>();
List<Face> changedFaces = new ArrayList<>(faces.size());
for (Face face : faces) {
changedFaces.add(face.withTexture(changed.computeIfAbsent(face.texture(), change)));
}
return new BlockModel(List.copyOf(changedFaces), coveredSides, hidesSameBlockSides);
}
// The side is split into a grid, and every cell has to lie under a face pressed flat against it.
private static boolean sideIsFilled(List<Face> faces, CubeFace side, boolean onlySolidFaces) {
boolean[] filled = new boolean[SIDE_CELLS * SIDE_CELLS];
for (Face face : faces) {
if (face.liesOn(side) && (!onlySolidFaces || face.texture().isFullyOpaque())) {
face.fillCells(side, filled, SIDE_CELLS);
}
}
for (boolean cell : filled) {
if (!cell) {
return false;
}
}
return true;
}
/**
* A parallelogram from {@code origin} along {@code edgeU} and {@code edgeV}. The texture point at the origin and
* how it moves along each edge are given as fractions of the texture.
*
* @param normal the way the face looks out; it is hidden when seen from behind
* @param cullFace the block side whose neighbour hides this face when it covers that side, or null
*/
record Face(Vec3 origin, Vec3 edgeU, Vec3 edgeV, Vec3 normal, BlockTexture texture, TextureMapping mapping,
double shade, CubeFace cullFace) {
// Laid out as seen from outside: u to the right and v downwards, matching Minecraft's UVs for a whole texture.
static Face boxSide(Vec3 from, Vec3 to, CubeFace side, BlockTexture texture, CubeFace cullFace) {
Vec3[] corners = BoxCorners.of(from, to, side);
return new Face(corners[0], corners[3].minus(corners[0]), corners[1].minus(corners[0]), side.direction(),
texture, TextureMapping.WHOLE, side.shade(), cullFace);
}
Face withTexture(BlockTexture changedTexture) {
return new Face(origin, edgeU, edgeV, normal, changedTexture, mapping, shade, cullFace);
}
Face transformed(Transform transform) {
Vec3 turnedNormal = transform.direction(normal);
double length = Math.sqrt(turnedNormal.x() * turnedNormal.x() + turnedNormal.y() * turnedNormal.y() + turnedNormal.z() * turnedNormal.z());
return new Face(transform.point(origin), transform.direction(edgeU), transform.direction(edgeV),
length == 0D ? turnedNormal : turnedNormal.times(1D / length), texture, mapping, shade,
cullFace == null ? null : CubeFace.nearest(transform.direction(cullFace.direction())));
}
Face withCullFace(CubeFace side) {
return new Face(origin, edgeU, edgeV, normal, texture, mapping, shade, side);
}
Face withShade(double changedShade) {
return new Face(origin, edgeU, edgeV, normal, texture, mapping, changedShade, cullFace);
}
private boolean liesOn(CubeFace side) {
int axis = side.axis();
double plane = side.pointsPositive() ? 1D : 0D;
return CubeFace.nearest(normal) == side
&& Math.abs(normal.component(axis)) > 1D - ON_SIDE_TOLERANCE
&& Math.abs(origin.component(axis) - plane) < ON_SIDE_TOLERANCE
&& Math.abs(edgeU.component(axis)) < ON_SIDE_TOLERANCE
&& Math.abs(edgeV.component(axis)) < ON_SIDE_TOLERANCE;
}
private void fillCells(CubeFace side, boolean[] filled, int cells) {
int firstAxis = side.axis() == 0 ? 1 : 0;
int secondAxis = side.axis() == 2 ? 1 : 2;
double[] firstRange = range(firstAxis);
double[] secondRange = range(secondAxis);
for (int first = 0; first < cells; first++) {
double firstCenter = (first + 0.5D) / cells;
if (firstCenter < firstRange[0] || firstCenter > firstRange[1]) {
continue;
}
for (int second = 0; second < cells; second++) {
double secondCenter = (second + 0.5D) / cells;
if (secondCenter >= secondRange[0] && secondCenter <= secondRange[1]) {
filled[first * cells + second] = true;
}
}
}
}
private double[] range(int axis) {
double start = origin.component(axis);
double alongU = edgeU.component(axis);
double alongV = edgeV.component(axis);
double low = start + Math.min(0D, alongU) + Math.min(0D, alongV);
double high = start + Math.max(0D, alongU) + Math.max(0D, alongV);
return new double[]{low, high};
}
}
/**
* Where a face's corner sits in its texture and how the texture point moves along the face's two edges, all as
* fractions of the texture's width and height.
*/
record TextureMapping(double u, double v, double uAlongU, double vAlongU, double uAlongV, double vAlongV) {
static final TextureMapping WHOLE = new TextureMapping(0D, 0D, 1D, 0D, 0D, 1D);
}
/**
* A box side's corners in Minecraft's order: top left, bottom left, bottom right and top right, as seen from
* outside the box.
*/
static final class BoxCorners {
private BoxCorners() {
}
static Vec3[] of(Vec3 from, Vec3 to, CubeFace side) {
double minX = from.x();
double minY = from.y();
double minZ = from.z();
double maxX = to.x();
double maxY = to.y();
double maxZ = to.z();
return switch (side) {
case DOWN -> corners(minX, minY, maxZ, minX, minY, minZ, maxX, minY, minZ, maxX, minY, maxZ);
case UP -> corners(minX, maxY, minZ, minX, maxY, maxZ, maxX, maxY, maxZ, maxX, maxY, minZ);
case NORTH -> corners(maxX, maxY, minZ, maxX, minY, minZ, minX, minY, minZ, minX, maxY, minZ);
case SOUTH -> corners(minX, maxY, maxZ, minX, minY, maxZ, maxX, minY, maxZ, maxX, maxY, maxZ);
case WEST -> corners(minX, maxY, minZ, minX, minY, minZ, minX, minY, maxZ, minX, maxY, maxZ);
case EAST -> corners(maxX, maxY, maxZ, maxX, minY, maxZ, maxX, minY, minZ, maxX, maxY, minZ);
};
}
private static Vec3[] corners(double... coordinates) {
Vec3[] corners = new Vec3[4];
for (int corner = 0; corner < 4; corner++) {
corners[corner] = new Vec3(coordinates[corner * 3], coordinates[corner * 3 + 1], coordinates[corner * 3 + 2]);
}
return corners;
}
}
}
@@ -0,0 +1,351 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.volmit.iris.core.gui.BlockModel.BoxCorners;
import com.volmit.iris.core.gui.BlockModel.Face;
import com.volmit.iris.core.gui.BlockModel.TextureMapping;
import com.volmit.iris.core.gui.BlockTextures.BlockStateId;
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
import com.volmit.iris.core.gui.BlockTextures.ModelDefinition;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Reads a block's look the way Minecraft does: every model part the blockstate picks, every box with its own texture
* and UVs, tilted boxes, the blockstate's turn with or without locked textures, and which faces a neighbour hides.
*/
final class BlockModelBuilder {
private static final double MODEL_UNITS = 16D;
private static final double BLOCK_CENTER = 0.5D;
private static final int QUARTER_TURN = 90;
private final BlockTextures textures;
private final EntityBlockShapes entityShapes;
BlockModelBuilder(BlockTextures textures) {
this.textures = textures;
this.entityShapes = new EntityBlockShapes(textures);
}
Optional<BlockModel> build(String blockState) {
try {
BlockStateId state = BlockStateId.parse(blockState);
List<JsonObject> placements = placementsOf(state);
if (placements == null) {
return Optional.empty();
}
List<Face> faces = new ArrayList<>();
for (JsonObject placement : placements) {
faces.addAll(placedFaces(state, placement));
}
faces.addAll(entityShapes.facesAddedTo(state));
return Optional.of(BlockModel.of(faces));
} catch (IOException | RuntimeException error) {
return Optional.empty();
}
}
/**
* A box with Minecraft's default UVs on each side, for shapes the game draws in code.
*/
static List<Face> box(Vec3 fromPixels, Vec3 toPixels, BlockTexture texture, boolean culledBySides) {
List<Face> faces = new ArrayList<>();
for (CubeFace side : CubeFace.values()) {
FaceUv uv = FaceUv.byDefault(side, fromPixels, toPixels);
Face face = boxFace(fromPixels, toPixels, side, texture, uv).withShade(side.shade());
faces.add(culledBySides ? face.withCullFace(side) : face);
}
return faces;
}
// A variant names one model; a multipart blockstate, like a fence's, adds a part for every condition that holds.
private List<JsonObject> placementsOf(BlockStateId state) throws IOException {
JsonObject definition = textures.readJson(BlockTextures.assetPath(state.namespace() + ":" + state.name(), "blockstates", ".json"));
if (definition == null) {
return null;
}
List<JsonObject> placements = new ArrayList<>();
if (definition.has("variants")) {
for (Map.Entry<String, JsonElement> variant : definition.getAsJsonObject("variants").entrySet()) {
if (BlockTextures.variantMatches(variant.getKey(), state.properties())) {
addPlacement(placements, variant.getValue());
break;
}
}
}
if (definition.has("multipart")) {
for (JsonElement part : definition.getAsJsonArray("multipart")) {
JsonObject rule = part.getAsJsonObject();
if (!rule.has("when") || BlockTextures.conditionMatches(rule.getAsJsonObject("when"), state.properties())) {
addPlacement(placements, rule.get("apply"));
}
}
}
return placements;
}
private static void addPlacement(List<JsonObject> placements, JsonElement models) {
JsonObject first = BlockTextures.firstModel(models);
if (first != null && first.has("model")) {
placements.add(first);
}
}
private List<Face> placedFaces(BlockStateId state, JsonObject placement) throws IOException {
ModelDefinition model = textures.readModel(placement.get("model").getAsString());
if (model.elements() == null) {
return entityShapes.facesOf(state, model);
}
BlockRotation rotation = BlockRotation.of(placement);
List<Face> faces = new ArrayList<>();
for (JsonElement element : model.elements()) {
addElementFaces(faces, state, model, element.getAsJsonObject(), rotation);
}
return faces;
}
private void addElementFaces(List<Face> faces, BlockStateId state, ModelDefinition model, JsonObject element, BlockRotation rotation) {
JsonObject elementFaces = element.getAsJsonObject("faces");
if (elementFaces == null) {
return;
}
Vec3 from = vector(element.getAsJsonArray("from"));
Vec3 to = vector(element.getAsJsonArray("to"));
Transform tilt = tiltOf(element);
boolean shaded = !element.has("shade") || element.get("shade").getAsBoolean();
CubeFace shadeSide = element.has("shade_direction_override")
? CubeFace.named(element.get("shade_direction_override").getAsString()) : null;
for (Map.Entry<String, JsonElement> entry : elementFaces.entrySet()) {
CubeFace side = CubeFace.named(entry.getKey());
JsonObject faceJson = entry.getValue().getAsJsonObject();
if (side == null || !faceJson.has("texture")) {
continue;
}
String textureName = BlockTextures.dereference(faceJson.get("texture").getAsString(), model.textures());
if (textureName == null) {
continue;
}
Optional<BlockTexture> texture = textures.faceTexture(state, textureName, BlockTextures.tintIndexOf(faceJson));
if (texture.isEmpty()) {
continue;
}
FaceUv uv = FaceUv.of(faceJson, side, from, to);
if (rotation.locksTextures()) {
uv = uv.lockedTo(side, rotation);
}
Face face = boxFace(from, to, side, texture.get(), uv).transformed(tilt);
if (faceJson.has("cullface")) {
face = face.withCullFace(CubeFace.named(faceJson.get("cullface").getAsString()));
}
face = face.transformed(rotation.transform());
faces.add(face.withShade(shadeOf(face, shaded, shadeSide, rotation)));
}
}
private static Face boxFace(Vec3 fromPixels, Vec3 toPixels, CubeFace side, BlockTexture texture, FaceUv uv) {
Vec3[] corners = BoxCorners.of(fromPixels.times(1D / MODEL_UNITS), toPixels.times(1D / MODEL_UNITS), side);
return new Face(corners[0], corners[3].minus(corners[0]), corners[1].minus(corners[0]), side.direction(),
texture, uv.mapping(), side.shade(), null);
}
private static double shadeOf(Face face, boolean shaded, CubeFace shadeSide, BlockRotation rotation) {
if (!shaded) {
return CubeFace.UP.shade();
}
if (shadeSide != null) {
return CubeFace.nearest(rotation.transform().direction(shadeSide.direction())).shade();
}
return CubeFace.nearest(face.normal()).shade();
}
// A box can be tilted around a point, on one axis or, since 1.21.?, on all three; rescaling stretches a tilted
// plant back to the full block width.
private static Transform tiltOf(JsonObject element) {
if (!element.has("rotation")) {
return Transform.IDENTITY;
}
JsonObject rotation = element.getAsJsonObject("rotation");
Vec3 origin = rotation.has("origin") ? vector(rotation.getAsJsonArray("origin")).times(1D / MODEL_UNITS) : new Vec3(BLOCK_CENTER, BLOCK_CENTER, BLOCK_CENTER);
Transform around = Transform.IDENTITY.translate(origin.x(), origin.y(), origin.z());
Transform back = Transform.IDENTITY.translate(-origin.x(), -origin.y(), -origin.z());
if (!rotation.has("axis")) {
return around
.rotateX(numberOr(rotation, "x", 0D))
.rotateY(numberOr(rotation, "y", 0D))
.rotateZ(numberOr(rotation, "z", 0D))
.then(back);
}
String axis = rotation.get("axis").getAsString();
double angle = numberOr(rotation, "angle", 0D);
boolean rescale = rotation.has("rescale") && rotation.get("rescale").getAsBoolean();
double stretch = rescale ? 1D / Math.cos(Math.toRadians(angle)) : 1D;
Transform stretched = around.scale(axis.equals("x") ? 1D : stretch, axis.equals("y") ? 1D : stretch, axis.equals("z") ? 1D : stretch);
Transform tilted = switch (axis) {
case "x" -> stretched.rotateX(angle);
case "y" -> stretched.rotateY(angle);
default -> stretched.rotateZ(angle);
};
return tilted.then(back);
}
private static double numberOr(JsonObject object, String key, double fallback) {
return object.has(key) ? object.get(key).getAsDouble() : fallback;
}
private static Vec3 vector(JsonArray values) {
return new Vec3(values.get(0).getAsDouble(), values.get(1).getAsDouble(), values.get(2).getAsDouble());
}
/**
* How a blockstate turns its model: around the x axis first, then the y axis, both in quarter turns against the
* right-hand rule. With uvlock the textures stay put while the shape turns.
*/
private record BlockRotation(int x, int y, boolean uvlock) {
static BlockRotation of(JsonObject placement) {
int x = placement.has("x") ? Math.floorMod(placement.get("x").getAsInt(), 360) : 0;
int y = placement.has("y") ? Math.floorMod(placement.get("y").getAsInt(), 360) : 0;
return new BlockRotation(x, y, placement.has("uvlock") && placement.get("uvlock").getAsBoolean());
}
boolean locksTextures() {
return uvlock && (x != 0 || y != 0);
}
Transform transform() {
return Transform.IDENTITY
.translate(BLOCK_CENTER, BLOCK_CENTER, BLOCK_CENTER)
.then(turn())
.translate(-BLOCK_CENTER, -BLOCK_CENTER, -BLOCK_CENTER);
}
Transform turn() {
return Transform.IDENTITY.rotateY(-y).rotateX(-x);
}
Transform inverseTurn() {
return Transform.IDENTITY.rotateX(x).rotateY(y);
}
}
/**
* A face's corner UVs in model pixels and the quarter turn of the texture on it.
*/
private record FaceUv(double u1, double v1, double u2, double v2, int rotation) {
static FaceUv of(JsonObject face, CubeFace side, Vec3 from, Vec3 to) {
int rotation = face.has("rotation") ? Math.floorMod(face.get("rotation").getAsInt(), 360) : 0;
if (!face.has("uv")) {
return byDefault(side, from, to).withRotation(rotation);
}
JsonArray uv = face.getAsJsonArray("uv");
return new FaceUv(uv.get(0).getAsDouble(), uv.get(1).getAsDouble(), uv.get(2).getAsDouble(), uv.get(3).getAsDouble(), rotation);
}
// The part of the texture the face would cover if the texture were laid over the whole block side.
static FaceUv byDefault(CubeFace side, Vec3 from, Vec3 to) {
double full = MODEL_UNITS;
return switch (side) {
case DOWN -> new FaceUv(from.x(), full - to.z(), to.x(), full - from.z(), 0);
case UP -> new FaceUv(from.x(), from.z(), to.x(), to.z(), 0);
case NORTH -> new FaceUv(full - to.x(), full - to.y(), full - from.x(), full - from.y(), 0);
case SOUTH -> new FaceUv(from.x(), full - to.y(), to.x(), full - from.y(), 0);
case WEST -> new FaceUv(from.z(), full - to.y(), to.z(), full - from.y(), 0);
case EAST -> new FaceUv(full - to.z(), full - to.y(), full - from.z(), full - from.y(), 0);
};
}
FaceUv withRotation(int changedRotation) {
return new FaceUv(u1, v1, u2, v2, changedRotation);
}
// Each corner takes the UV a quarter turn further along for every quarter turn of the texture.
TextureMapping mapping() {
double[] origin = cornerUv(0);
double[] alongU = cornerUv(3);
double[] alongV = cornerUv(1);
return new TextureMapping(origin[0] / MODEL_UNITS, origin[1] / MODEL_UNITS,
(alongU[0] - origin[0]) / MODEL_UNITS, (alongU[1] - origin[1]) / MODEL_UNITS,
(alongV[0] - origin[0]) / MODEL_UNITS, (alongV[1] - origin[1]) / MODEL_UNITS);
}
private double[] cornerUv(int corner) {
return switch ((corner + rotation / QUARTER_TURN) % 4) {
case 0 -> new double[]{u1, v1};
case 1 -> new double[]{u1, v2};
case 2 -> new double[]{u2, v2};
default -> new double[]{u2, v1};
};
}
/**
* Minecraft's uvlock: the UVs are carried through the reverse of the blockstate's turn, so the texture keeps
* its place in the world while the face moves to another side.
*/
FaceUv lockedTo(CubeFace side, BlockRotation blockRotation) {
CubeFace turnedSide = CubeFace.nearest(blockRotation.turn().direction(side.direction()));
Transform uvTurn = Transform.IDENTITY
.translate(BLOCK_CENTER, BLOCK_CENTER, BLOCK_CENTER)
.then(faceFromWorld(side))
.then(blockRotation.inverseTurn())
.then(worldFromFace(turnedSide))
.translate(-BLOCK_CENTER, -BLOCK_CENTER, -BLOCK_CENTER);
Vec3 first = uvTurn.point(new Vec3(u1 / MODEL_UNITS, v1 / MODEL_UNITS, 0D)).times(MODEL_UNITS);
Vec3 second = uvTurn.point(new Vec3(u2 / MODEL_UNITS, v2 / MODEL_UNITS, 0D)).times(MODEL_UNITS);
boolean keepsUOrder = Math.signum(u2 - u1) == Math.signum(second.x() - first.x());
boolean keepsVOrder = Math.signum(v2 - v1) == Math.signum(second.y() - first.y());
Vec3 textureRight = uvTurn.direction(new Vec3(Math.cos(Math.toRadians(rotation)), Math.sin(Math.toRadians(rotation)), 0D));
long quarterTurns = Math.round(Math.toDegrees(Math.atan2(textureRight.y(), textureRight.x())) / QUARTER_TURN);
return new FaceUv(
keepsUOrder ? first.x() : second.x(), keepsVOrder ? first.y() : second.y(),
keepsUOrder ? second.x() : first.x(), keepsVOrder ? second.y() : first.y(),
(int) Math.floorMod(-quarterTurns * QUARTER_TURN, 360L));
}
// Minecraft lays every side's texture out as if it were the south side, turned into place.
private static Transform worldFromFace(CubeFace side) {
return switch (side) {
case SOUTH -> Transform.IDENTITY;
case EAST -> Transform.IDENTITY.rotateY(90D);
case WEST -> Transform.IDENTITY.rotateY(-90D);
case NORTH -> Transform.IDENTITY.rotateY(180D);
case UP -> Transform.IDENTITY.rotateX(-90D);
case DOWN -> Transform.IDENTITY.rotateX(90D);
};
}
private static Transform faceFromWorld(CubeFace side) {
return switch (side) {
case SOUTH -> Transform.IDENTITY;
case EAST -> Transform.IDENTITY.rotateY(-90D);
case WEST -> Transform.IDENTITY.rotateY(90D);
case NORTH -> Transform.IDENTITY.rotateY(180D);
case UP -> Transform.IDENTITY.rotateX(90D);
case DOWN -> Transform.IDENTITY.rotateX(-90D);
};
}
}
}
@@ -0,0 +1,101 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import org.bukkit.block.data.BlockData;
import java.awt.Color;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
final class BlockPreviewColors {
private static final Method MAP_COLOR_METHOD = findMapColorMethod();
private static final double[] MAP_SHADES = {1D, 0.86D, 0.71D, 0.53D};
private static final int NO_MAP_COLOR = 0;
private BlockPreviewColors() {
}
static Map<String, Integer> colorsByBlock(List<BlockData> blocks) {
Map<String, Integer> colors = new HashMap<>();
List<Integer> usedColors = new ArrayList<>();
for (BlockData block : blocks) {
String key = block.getAsString(true);
if (colors.containsKey(key)) {
continue;
}
int baseColor = baseColor(block);
int color = firstUnusedShade(baseColor, usedColors);
usedColors.add(color);
colors.put(key, color);
}
return colors;
}
// Blocks that share a map colour, like sand and sandstone, get darker shades the way Minecraft maps shade them.
private static int firstUnusedShade(int baseColor, List<Integer> usedColors) {
for (double shade : MAP_SHADES) {
int shaded = shade(baseColor, shade);
if (!usedColors.contains(shaded)) {
return shaded;
}
}
return baseColor;
}
private static int baseColor(BlockData block) {
int mapColor = mapColor(block);
return mapColor == NO_MAP_COLOR ? colorFromName(block.getMaterial().getKey().getKey()) : mapColor;
}
private static int mapColor(BlockData block) {
if (MAP_COLOR_METHOD == null) {
return NO_MAP_COLOR;
}
try {
return ((org.bukkit.Color) MAP_COLOR_METHOD.invoke(block)).asRGB();
} catch (ReflectiveOperationException | RuntimeException error) {
return NO_MAP_COLOR;
}
}
private static int colorFromName(String name) {
float hue = (name.hashCode() & 0xFFFF) / (float) 0xFFFF;
return Color.HSBtoRGB(hue, 0.45F, 0.75F) & 0xFFFFFF;
}
static int shade(int color, double shade) {
int red = (int) Math.min(255, ((color >>> 16) & 0xFF) * shade);
int green = (int) Math.min(255, ((color >>> 8) & 0xFF) * shade);
int blue = (int) Math.min(255, (color & 0xFF) * shade);
return (red << 16) | (green << 8) | blue;
}
// Iris compiles against the 1.20.1 API, which predates BlockData#getMapColor.
private static Method findMapColorMethod() {
try {
return BlockData.class.getMethod("getMapColor");
} catch (NoSuchMethodException error) {
return null;
}
}
}
@@ -0,0 +1,603 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.bukkit.block.data.Bisected;
import org.bukkit.block.data.BlockData;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.ZipFile;
final class BlockTextures {
private static final String DEFAULT_NAMESPACE = "minecraft";
private static final int MAX_MODEL_PARENTS = 16;
private static final int MAX_TEXTURE_REFERENCES = 16;
static final int NO_TINT_INDEX = -1;
private static final Object SHARED_LOCK = new Object();
private static CompletableFuture<BlockTextures> shared;
private final ZipFile clientJar;
private final MinecraftBiomeColors biomeColors;
private final Map<String, Optional<ResolvedTexture>> resolvedTextures = new ConcurrentHashMap<>();
private final Map<String, Optional<BlockModel>> resolvedModels = new ConcurrentHashMap<>();
private final Map<String, Optional<BlockTexture>> faceTextures = new ConcurrentHashMap<>();
private final BlockModelBuilder modelBuilder = new BlockModelBuilder(this);
private BlockTextures(ZipFile clientJar, MinecraftBiomeColors biomeColors) {
this.clientJar = clientJar;
this.biomeColors = biomeColors;
}
static CompletableFuture<BlockTextures> shared() {
synchronized (SHARED_LOCK) {
if (shared == null || shared.isCompletedExceptionally()) {
shared = MinecraftClientJar.open().thenApply(BlockTextures::load);
}
return shared;
}
}
private static BlockTextures load(ZipFile clientJar) {
try {
return new BlockTextures(clientJar, MinecraftBiomeColors.load(clientJar));
} catch (IOException error) {
throw new IllegalStateException("Could not read Minecraft's biome colour maps", error);
}
}
MinecraftBiomeColors biomeColors() {
return biomeColors;
}
/**
* What the block looks like from above. Plants and other side-on sprites become their average colour, because
* their sprite would read as stripes across a stack of ground blocks.
*/
Optional<BlockTexture> topTexture(BlockData block) {
return resolve(block).map(resolved -> resolved.isSideOnSprite() ? resolved.texture().flattened() : resolved.texture());
}
/**
* Like {@link #topTexture}, but plants keep their sprite, so single decorations on the ground stay recognisable.
* Two block tall plants give their bottom half, the part standing on the ground.
*/
Optional<BlockTexture> iconTexture(BlockData block) {
BlockData standing = block instanceof Bisected ? withHalf(block, Bisected.Half.BOTTOM) : block;
return resolve(standing).map(ResolvedTexture::texture);
}
/**
* The sprite of a two block tall plant's top half, or nothing for any other block.
*/
Optional<BlockTexture> upperHalfIcon(BlockData block) {
if (!(block instanceof Bisected)) {
return Optional.empty();
}
return resolve(withHalf(block, Bisected.Half.TOP)).filter(ResolvedTexture::isSideOnSprite).map(ResolvedTexture::texture);
}
/**
* What the block looks like from every side, for drawing it in 3D.
*/
Optional<BlockModel> model(BlockData block) {
return resolvedModels.computeIfAbsent(block.getAsString(false), modelBuilder::build);
}
private static BlockData withHalf(BlockData block, Bisected.Half half) {
BlockData copy = block.clone();
((Bisected) copy).setHalf(half);
return copy;
}
// Blockstate files match on every property, so defaults the pack left out have to be spelled out too.
private Optional<ResolvedTexture> resolve(BlockData block) {
return resolvedTextures.computeIfAbsent(block.getAsString(false), this::resolveTopTexture);
}
private Optional<ResolvedTexture> resolveTopTexture(String blockState) {
try {
BlockStateId state = BlockStateId.parse(blockState);
JsonObject variant = variantOf(state);
if (variant == null || !variant.has("model")) {
return Optional.empty();
}
ModelFace face = readModel(variant.get("model").getAsString()).visibleFace(faceSeenFromAbove(variant));
if (face == null) {
return Optional.empty();
}
return readTexture(state, face).map(texture -> new ResolvedTexture(texture, face.isSideOnSprite()));
} catch (IOException | RuntimeException error) {
return Optional.empty();
}
}
private JsonObject variantOf(BlockStateId state) throws IOException {
JsonObject definition = readJson(assetPath(state.namespace() + ":" + state.name(), "blockstates", ".json"));
return definition == null ? null : pickVariant(definition, state.properties());
}
private static int rotationOf(JsonObject variant, String axis) {
return variant.has(axis) ? Math.floorMod(variant.get(axis).getAsInt(), 360) : 0;
}
/**
* One model face's texture with the block's tint, shared by every block state that uses it.
*/
Optional<BlockTexture> faceTexture(BlockStateId state, String texture, int tintIndex) {
int fixedTint = MinecraftBlockTints.fixedTint(state.name(), state.properties(), tintIndex);
BiomeTint biomeTint = MinecraftBlockTints.biomeTint(state.name(), tintIndex).orElse(null);
return faceTextures.computeIfAbsent(texture + "|" + fixedTint + "|" + biomeTint, key -> {
try {
return readTexture(state, new ModelFace(List.of(texture), tintIndex, false));
} catch (IOException | RuntimeException error) {
return Optional.empty();
}
});
}
/**
* A texture drawn on a block entity, like a chest, whole and tinted with a fixed colour.
*/
Optional<BlockTexture> entityTexture(String texture, int tint) {
return faceTextures.computeIfAbsent(texture + "|entity|" + tint, key -> {
try {
BufferedImage image = readImage(assetPath(texture, "textures", ".png"));
return image == null ? Optional.empty() : Optional.of(BlockTexture.fromWholeImage(image, tint));
} catch (IOException | RuntimeException error) {
return Optional.empty();
}
});
}
private Optional<BlockTexture> readTexture(BlockStateId state, ModelFace face) throws IOException {
BufferedImage image = readLayeredImage(face.textures());
if (image == null) {
return Optional.empty();
}
try {
return Optional.of(BlockTexture.fromImage(
image,
MinecraftBlockTints.fixedTint(state.name(), state.properties(), face.tintIndex()),
MinecraftBlockTints.biomeTint(state.name(), face.tintIndex()).orElse(null)
));
} catch (IllegalArgumentException invisibleTexture) {
return Optional.empty();
}
}
// A block rotated on its x axis, like a sideways log, shows a side face on top. Blockstates turn the model
// against the right-hand rule, so 90 degrees brings the south side up.
private static String faceSeenFromAbove(JsonObject variant) {
return switch (rotationOf(variant, "x")) {
case 90 -> "south";
case 270 -> "north";
case 180 -> "down";
default -> "up";
};
}
ModelDefinition readModel(String modelReference) throws IOException {
Map<String, String> textures = new HashMap<>();
JsonArray elements = null;
String current = modelReference;
for (int depth = 0; current != null && depth < MAX_MODEL_PARENTS; depth++) {
JsonObject model = readJson(assetPath(current, "models", ".json"));
if (model == null) {
break;
}
if (model.has("textures")) {
for (Map.Entry<String, JsonElement> texture : model.getAsJsonObject("textures").entrySet()) {
textures.putIfAbsent(texture.getKey(), spriteName(texture.getValue()));
}
}
if (elements == null && model.has("elements")) {
elements = model.getAsJsonArray("elements");
}
current = model.has("parent") ? model.get("parent").getAsString() : null;
}
return new ModelDefinition(textures, elements);
}
// Since 1.21.? a texture can also be an object naming its sprite, like glass's {"sprite": ..., "force_translucent": true}.
private static String spriteName(JsonElement texture) {
return texture.isJsonObject() ? texture.getAsJsonObject().get("sprite").getAsString() : texture.getAsString();
}
// Some plants draw a second sprite over the first, like the glowing fireflies on a firefly bush.
private static List<String> spriteLayers(JsonArray elements, Map<String, String> textures) {
Set<String> layers = new LinkedHashSet<>();
for (JsonElement element : elements) {
JsonObject faces = element.getAsJsonObject().getAsJsonObject("faces");
if (faces == null) {
continue;
}
for (Map.Entry<String, JsonElement> face : faces.entrySet()) {
String texture = dereference(face.getValue().getAsJsonObject().get("texture").getAsString(), textures);
if (texture != null) {
layers.add(texture);
}
}
}
return List.copyOf(layers);
}
private static int firstTintIndex(JsonArray elements) {
for (JsonElement element : elements) {
JsonObject faces = element.getAsJsonObject().getAsJsonObject("faces");
if (faces == null) {
continue;
}
for (Map.Entry<String, JsonElement> face : faces.entrySet()) {
int tintIndex = tintIndexOf(face.getValue().getAsJsonObject());
if (tintIndex != NO_TINT_INDEX) {
return tintIndex;
}
}
}
return NO_TINT_INDEX;
}
static int tintIndexOf(JsonObject face) {
return face.has("tintindex") ? face.get("tintindex").getAsInt() : NO_TINT_INDEX;
}
private static JsonObject topmostFace(JsonArray elements, String faceName) {
JsonObject best = null;
double bestHeight = Double.NEGATIVE_INFINITY;
double bestArea = -1D;
for (JsonElement element : elements) {
JsonObject cuboid = element.getAsJsonObject();
JsonObject faces = cuboid.getAsJsonObject("faces");
if (faces == null || !faces.has(faceName)) {
continue;
}
JsonArray from = cuboid.getAsJsonArray("from");
JsonArray to = cuboid.getAsJsonArray("to");
double height = to.get(1).getAsDouble();
double area = (to.get(0).getAsDouble() - from.get(0).getAsDouble()) * (to.get(2).getAsDouble() - from.get(2).getAsDouble());
if (height > bestHeight || (height == bestHeight && area > bestArea)) {
best = faces.getAsJsonObject(faceName);
bestHeight = height;
bestArea = area;
}
}
return best;
}
static String dereference(String reference, Map<String, String> textures) {
String resolved = reference;
for (int hop = 0; resolved != null && resolved.startsWith("#") && hop < MAX_TEXTURE_REFERENCES; hop++) {
resolved = textures.get(resolved.substring(1));
}
return resolved == null || resolved.startsWith("#") ? null : resolved;
}
private static JsonObject pickVariant(JsonObject definition, Map<String, String> properties) {
if (definition.has("variants")) {
for (Map.Entry<String, JsonElement> variant : definition.getAsJsonObject("variants").entrySet()) {
if (variantMatches(variant.getKey(), properties)) {
return firstModel(variant.getValue());
}
}
return null;
}
if (definition.has("multipart")) {
for (JsonElement part : definition.getAsJsonArray("multipart")) {
JsonObject rule = part.getAsJsonObject();
if (!rule.has("when") || conditionMatches(rule.getAsJsonObject("when"), properties)) {
return firstModel(rule.get("apply"));
}
}
}
return null;
}
static boolean variantMatches(String variantKey, Map<String, String> properties) {
if (variantKey.isEmpty()) {
return true;
}
for (String condition : variantKey.split(",")) {
String[] pair = condition.split("=", 2);
if (pair.length != 2 || !pair[1].equals(properties.get(pair[0]))) {
return false;
}
}
return true;
}
static boolean conditionMatches(JsonObject condition, Map<String, String> properties) {
if (condition.has("OR")) {
for (JsonElement option : condition.getAsJsonArray("OR")) {
if (conditionMatches(option.getAsJsonObject(), properties)) {
return true;
}
}
return false;
}
if (condition.has("AND")) {
for (JsonElement requirement : condition.getAsJsonArray("AND")) {
if (!conditionMatches(requirement.getAsJsonObject(), properties)) {
return false;
}
}
return true;
}
for (Map.Entry<String, JsonElement> requirement : condition.entrySet()) {
String actual = properties.get(requirement.getKey());
boolean anyAllowed = false;
for (String allowed : requirement.getValue().getAsString().split("\\|")) {
anyAllowed |= allowed.equals(actual);
}
if (!anyAllowed) {
return false;
}
}
return true;
}
static JsonObject firstModel(JsonElement models) {
if (models.isJsonArray()) {
JsonArray choices = models.getAsJsonArray();
return choices.isEmpty() ? null : choices.get(0).getAsJsonObject();
}
return models.getAsJsonObject();
}
static String assetPath(String resource, String folder, String extension) {
int separator = resource.indexOf(':');
String namespace = separator < 0 ? DEFAULT_NAMESPACE : resource.substring(0, separator);
String path = separator < 0 ? resource : resource.substring(separator + 1);
return "assets/" + namespace + "/" + folder + "/" + path + extension;
}
JsonObject readJson(String path) throws IOException {
return ClientJarFiles.readJson(clientJar, path);
}
// Each layer is drawn over the ones before it, using only its first frame when it is animated.
private BufferedImage readLayeredImage(List<String> layers) throws IOException {
BufferedImage bottom = readImage(assetPath(layers.get(0), "textures", ".png"));
if (bottom == null || layers.size() == 1) {
return bottom;
}
int size = bottom.getWidth();
BufferedImage combined = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D canvas = combined.createGraphics();
try {
for (String layer : layers) {
BufferedImage image = readImage(assetPath(layer, "textures", ".png"));
if (image != null) {
int frameSize = image.getWidth();
canvas.drawImage(image, 0, 0, size, size, 0, 0, frameSize, frameSize, null);
}
}
} finally {
canvas.dispose();
}
return combined;
}
private BufferedImage readImage(String path) throws IOException {
return ClientJarFiles.readImage(clientJar, path);
}
// Plants and other blocks without a top face only have a side-on sprite, which would read as stripes from above.
private record ModelFace(List<String> textures, int tintIndex, boolean isSideOnSprite) {
}
/**
* A block model with its parents' textures merged in. Models drawn in code, like chests, have no elements.
*/
record ModelDefinition(Map<String, String> textures, JsonArray elements) {
ModelFace visibleFace(String faceName) {
ModelFace face = sideFace(faceName);
if (face != null) {
return face;
}
return elements != null && !spriteLayers(elements, textures).isEmpty() ? spriteFace() : particleFace();
}
// The face's texture on the model's topmost box, the particle texture for models without boxes, or null when
// the model leaves that side open.
ModelFace sideFace(String faceName) {
if (elements == null) {
return particleFace();
}
JsonObject face = topmostFace(elements, faceName);
if (face == null) {
return null;
}
String texture = dereference(face.get("texture").getAsString(), textures);
return texture == null ? null : new ModelFace(List.of(texture), tintIndexOf(face), false);
}
ModelFace spriteFace() {
return new ModelFace(spriteLayers(elements, textures), firstTintIndex(elements), true);
}
private ModelFace particleFace() {
String particle = dereference("#particle", textures);
return particle == null ? null : new ModelFace(List.of(particle), NO_TINT_INDEX, false);
}
}
private record ResolvedTexture(BlockTexture texture, boolean isSideOnSprite) {
}
record BlockStateId(String namespace, String name, Map<String, String> properties) {
static BlockStateId parse(String blockState) {
int propertiesStart = blockState.indexOf('[');
String id = propertiesStart < 0 ? blockState : blockState.substring(0, propertiesStart);
int separator = id.indexOf(':');
String namespace = separator < 0 ? DEFAULT_NAMESPACE : id.substring(0, separator);
String name = separator < 0 ? id : id.substring(separator + 1);
Map<String, String> properties = new HashMap<>();
if (propertiesStart >= 0) {
String body = blockState.substring(propertiesStart + 1, blockState.length() - 1);
for (String pair : body.split(",")) {
String[] keyValue = pair.split("=", 2);
if (keyValue.length == 2) {
properties.put(keyValue[0], keyValue[1]);
}
}
}
return new BlockStateId(namespace, name, properties);
}
}
// Biome tinted textures keep their grey pixels, because the biome colour changes from column to column.
// See-through pixels get a darker shade of the average colour, and the opaque mask remembers which they were.
// Textures with partly see-through pixels, like stained glass, ice and water, keep each pixel's alpha too.
record BlockTexture(int width, int height, int[] pixels, boolean[] opaque, int[] alphas, int averageColor,
BiomeTint biomeTint) {
private static final double GAP_SHADE = 0.55D;
private static final int FULL_ALPHA = 255;
// Animated textures stack their frames vertically, so only the first square frame is used.
static BlockTexture fromImage(BufferedImage image, int fixedTint, BiomeTint biomeTint) {
return fromPixels(image, image.getWidth(), Math.min(image.getWidth(), image.getHeight()), fixedTint, biomeTint);
}
// Entity textures, like a chest's, are laid out for a model and can be wider than they are tall.
static BlockTexture fromWholeImage(BufferedImage image, int fixedTint) {
return fromPixels(image, image.getWidth(), image.getHeight(), fixedTint, null);
}
static BlockTexture solid(int color) {
return new BlockTexture(1, 1, new int[]{color}, new boolean[]{true}, null, color, null);
}
private static BlockTexture fromPixels(BufferedImage image, int width, int height, int fixedTint, BiomeTint biomeTint) {
int[] pixels = new int[width * height];
boolean[] opaque = new boolean[pixels.length];
int[] alphas = new int[pixels.length];
boolean translucent = false;
long red = 0;
long green = 0;
long blue = 0;
int opaqueCount = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int argb = image.getRGB(x, y);
int index = y * width + x;
int alpha = argb >>> 24;
alphas[index] = alpha;
if (alpha == 0) {
continue;
}
translucent |= alpha < FULL_ALPHA;
int color = multiply(argb & 0xFFFFFF, fixedTint);
pixels[index] = color;
opaque[index] = true;
red += (color >>> 16) & 0xFF;
green += (color >>> 8) & 0xFF;
blue += color & 0xFF;
opaqueCount++;
}
}
if (opaqueCount == 0) {
throw new IllegalArgumentException("Texture has no visible pixels");
}
int average = (int) (red / opaqueCount) << 16 | (int) (green / opaqueCount) << 8 | (int) (blue / opaqueCount);
int gapColor = BlockPreviewColors.shade(average, GAP_SHADE);
for (int index = 0; index < pixels.length; index++) {
if (!opaque[index]) {
pixels[index] = gapColor;
}
}
return new BlockTexture(width, height, pixels, opaque, translucent ? alphas : null, average, biomeTint);
}
boolean isTranslucent() {
return alphas != null;
}
boolean isFullyOpaque() {
if (isTranslucent()) {
return false;
}
for (boolean pixelOpaque : opaque) {
if (!pixelOpaque) {
return false;
}
}
return true;
}
BlockTexture flattened() {
int[] flat = new int[pixels.length];
Arrays.fill(flat, averageColor);
boolean[] allOpaque = new boolean[pixels.length];
Arrays.fill(allOpaque, true);
return new BlockTexture(width, height, flat, allOpaque, null, averageColor, biomeTint);
}
BlockTexture tinted(int tint) {
int[] tintedPixels = new int[pixels.length];
for (int index = 0; index < pixels.length; index++) {
tintedPixels[index] = multiply(pixels[index], tint);
}
return new BlockTexture(width, height, tintedPixels, opaque, alphas, multiply(averageColor, tint), null);
}
int texelAt(double worldX, double worldZ) {
return pixels[texelIndex(worldX, worldZ)];
}
boolean isOpaqueAt(double worldX, double worldZ) {
return opaque[texelIndex(worldX, worldZ)];
}
private int texelIndex(double worldX, double worldZ) {
int x = Math.min(width - 1, (int) ((worldX - Math.floor(worldX)) * width));
int z = Math.min(height - 1, (int) ((worldZ - Math.floor(worldZ)) * height));
return z * width + x;
}
BufferedImage toImage() {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
image.setRGB(0, 0, width, height, pixels, 0, width);
return image;
}
// The GPU multiplies the texture by the tint in floating point and rounds back to 8 bits per channel.
static int multiply(int color, int tint) {
return multiplyChannel(color, tint, 16) << 16 | multiplyChannel(color, tint, 8) << 8 | multiplyChannel(color, tint, 0);
}
private static int multiplyChannel(int color, int tint, int shift) {
return (((color >>> shift) & 0xFF) * ((tint >>> shift) & 0xFF) + 127) / 255;
}
}
}
@@ -0,0 +1,82 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import javax.imageio.ImageIO;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
final class ClientJarFiles {
private ClientJarFiles() {
}
static JsonObject readJson(ZipFile clientJar, String path) throws IOException {
ZipEntry entry = clientJar.getEntry(path);
if (entry == null) {
return null;
}
try (InputStreamReader reader = new InputStreamReader(clientJar.getInputStream(entry), StandardCharsets.UTF_8)) {
return JsonParser.parseReader(reader).getAsJsonObject();
}
}
static BufferedImage readImage(ZipFile clientJar, String path) throws IOException {
ZipEntry entry = clientJar.getEntry(path);
if (entry == null) {
return null;
}
try (InputStream input = clientJar.getInputStream(entry)) {
BufferedImage image = ImageIO.read(input);
return image != null && isGrey(image) ? withGreyKept(image) : image;
}
}
private static boolean isGrey(BufferedImage image) {
return image.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_GRAY;
}
// Java treats grey PNGs, like stone's, as linear light and brightens them when converting to RGB; Minecraft reads
// the stored values as they are.
private static BufferedImage withGreyKept(BufferedImage image) {
Raster raster = image.getRaster();
boolean hasAlpha = raster.getNumBands() > 1;
int greyMax = (1 << raster.getSampleModel().getSampleSize(0)) - 1;
int alphaMax = hasAlpha ? (1 << raster.getSampleModel().getSampleSize(1)) - 1 : 1;
BufferedImage converted = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
int grey = raster.getSample(x, y, 0) * 255 / greyMax;
int alpha = hasAlpha ? raster.getSample(x, y, 1) * 255 / alphaMax : 255;
converted.setRGB(x, y, alpha << 24 | grey << 16 | grey << 8 | grey);
}
}
return converted;
}
}
@@ -0,0 +1,114 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
/**
* The six sides of a block, named as in Minecraft's model files, with the light Minecraft gives each side.
*/
enum CubeFace {
UP("up", 0, 1, 0, 1.0D),
DOWN("down", 0, -1, 0, 0.5D),
NORTH("north", 0, 0, -1, 0.8D),
SOUTH("south", 0, 0, 1, 0.8D),
EAST("east", 1, 0, 0, 0.6D),
WEST("west", -1, 0, 0, 0.6D);
private final String modelName;
private final int stepX;
private final int stepY;
private final int stepZ;
private final double shade;
CubeFace(String modelName, int stepX, int stepY, int stepZ, double shade) {
this.modelName = modelName;
this.stepX = stepX;
this.stepY = stepY;
this.stepZ = stepZ;
this.shade = shade;
}
static CubeFace named(String modelName) {
for (CubeFace face : values()) {
if (face.modelName.equals(modelName)) {
return face;
}
}
return null;
}
/**
* The side a direction points at most, like Minecraft picks for a tilted face.
*/
static CubeFace nearest(Vec3 direction) {
CubeFace nearest = UP;
double best = Double.NEGATIVE_INFINITY;
for (CubeFace face : values()) {
double alignment = face.stepX * direction.x() + face.stepY * direction.y() + face.stepZ * direction.z();
if (alignment > best) {
best = alignment;
nearest = face;
}
}
return nearest;
}
String modelName() {
return modelName;
}
int stepX() {
return stepX;
}
int stepY() {
return stepY;
}
int stepZ() {
return stepZ;
}
double shade() {
return shade;
}
Vec3 direction() {
return new Vec3(stepX, stepY, stepZ);
}
CubeFace opposite() {
return switch (this) {
case UP -> DOWN;
case DOWN -> UP;
case NORTH -> SOUTH;
case SOUTH -> NORTH;
case EAST -> WEST;
case WEST -> EAST;
};
}
int axis() {
return stepX != 0 ? 0 : stepY != 0 ? 1 : 2;
}
boolean pointsPositive() {
return stepX + stepY + stepZ > 0;
}
}
@@ -0,0 +1,298 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.core.gui.BlockModel.Face;
import com.volmit.iris.core.gui.BlockModel.TextureMapping;
import com.volmit.iris.core.gui.BlockTextures.BlockStateId;
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
import com.volmit.iris.core.gui.BlockTextures.ModelDefinition;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/**
* Shapes for the blocks Minecraft draws in code instead of from a model file, like chests, heads, banners and
* fluids, which fill their block. Chests, heads, banners and the bell use their entity textures laid out the way Minecraft's model parts are;
* rarer ones are a simple box in their particle texture.
*/
final class EntityBlockShapes {
private static final Set<String> INVISIBLE = Set.of("air", "cave_air", "void_air", "barrier", "light",
"structure_void", "moving_piston", "pitcher_crop");
private static final int END_PORTAL_COLOR = 0x0E1219;
private static final double SKULL_WALL_OFFSET = 0.25D;
private static final double ROTATION_SEGMENT_DEGREES = 22.5D;
private static final double BANNER_SCALE = 2D / 3D;
private static final double MODEL_UNITS = 16D;
private static final Map<String, Integer> DYE_COLORS = Map.ofEntries(
Map.entry("white", 0xF9FFFE), Map.entry("orange", 0xF9801D), Map.entry("magenta", 0xC74EBD),
Map.entry("light_blue", 0x3AB3DA), Map.entry("yellow", 0xFED83D), Map.entry("lime", 0x80C71F),
Map.entry("pink", 0xF38BAA), Map.entry("gray", 0x474F52), Map.entry("light_gray", 0x9D9D97),
Map.entry("cyan", 0x169C9C), Map.entry("purple", 0x8932B8), Map.entry("blue", 0x3C44AA),
Map.entry("brown", 0x835432), Map.entry("green", 0x5E7C16), Map.entry("red", 0xB02E26),
Map.entry("black", 0x1D1D21));
private final BlockTextures textures;
EntityBlockShapes(BlockTextures textures) {
this.textures = textures;
}
/**
* The faces of a block whose model has no boxes of its own.
*/
List<Face> facesOf(BlockStateId state, ModelDefinition model) {
String name = state.name();
if (INVISIBLE.contains(name)) {
return List.of();
}
if (name.endsWith("chest")) {
return chest(state);
}
if (name.endsWith("_skull") || name.endsWith("_head")) {
return head(state, model);
}
if (name.endsWith("banner")) {
return banner(state);
}
return switch (name) {
case "conduit" -> particleBox(state, model, new Vec3(5, 5, 5), new Vec3(11, 11, 11), false);
case "decorated_pot" -> decoratedPot(state, model);
case "end_portal" -> BlockModelBuilder.box(new Vec3(0, 0, 0), new Vec3(16, 12, 16), BlockTexture.solid(END_PORTAL_COLOR), true);
case "end_gateway" -> BlockModelBuilder.box(new Vec3(0, 0, 0), new Vec3(16, 16, 16), BlockTexture.solid(END_PORTAL_COLOR), true);
default -> name.endsWith("copper_golem_statue")
? particleBox(state, model, new Vec3(4, 0, 4), new Vec3(12, 14, 12), false)
: particleBox(state, model, new Vec3(0, 0, 0), new Vec3(16, 16, 16), true);
};
}
/**
* Parts a block draws in code on top of its model, like the bell hanging in its frame.
*/
List<Face> facesAddedTo(BlockStateId state) {
if (!state.name().equals("bell")) {
return List.of();
}
Optional<BlockTexture> texture = textures.entityTexture("entity/bell/bell_body", MinecraftBlockTints.NO_TINT);
if (texture.isEmpty()) {
return List.of();
}
List<Face> faces = new ArrayList<>();
faces.addAll(modelPartBox(texture.get(), 0, 0, new Vec3(5, 6, 5), new Vec3(6, 7, 6), Transform.IDENTITY));
faces.addAll(modelPartBox(texture.get(), 0, 13, new Vec3(4, 4, 4), new Vec3(8, 2, 8), Transform.IDENTITY));
return faces;
}
private List<Face> particleBox(BlockStateId state, ModelDefinition model, Vec3 from, Vec3 to, boolean culledBySides) {
String particle = BlockTextures.dereference("#particle", model.textures());
if (particle == null) {
return List.of();
}
return textures.faceTexture(state, particle, BlockTextures.NO_TINT_INDEX)
.map(texture -> BlockModelBuilder.box(from, to, texture, culledBySides))
.orElse(List.of());
}
private List<Face> decoratedPot(BlockStateId state, ModelDefinition model) {
List<Face> faces = new ArrayList<>(particleBox(state, model, new Vec3(1, 0, 1), new Vec3(15, 16, 15), false));
faces.addAll(particleBox(state, model, new Vec3(4, 16, 4), new Vec3(12, 20, 12), false));
return faces;
}
// Single chests are 14 pixels wide; each half of a double chest is 15, reaching over to its partner.
private List<Face> chest(BlockStateId state) {
String type = state.properties().getOrDefault("type", "single");
String suffix = type.equals("single") ? "" : "_" + type;
Optional<BlockTexture> texture = textures.entityTexture("entity/chest/" + chestTextureName(state.name()) + suffix, MinecraftBlockTints.NO_TINT);
if (texture.isEmpty()) {
return List.of();
}
Transform place = Transform.IDENTITY
.translate(0.5D, 0.5D, 0.5D)
.rotateY(-yRotationOf(state.properties().getOrDefault("facing", "north")))
.translate(-0.5D, -0.5D, -0.5D);
double left = type.equals("left") ? 0D : 1D;
double width = type.equals("single") ? 14D : 15D;
double lockX = switch (type) {
case "left" -> 0D;
case "right" -> 15D;
default -> 7D;
};
double lockWidth = type.equals("single") ? 2D : 1D;
List<Face> faces = new ArrayList<>();
faces.addAll(modelPartBox(texture.get(), 0, 19, new Vec3(left, 0, 1), new Vec3(width, 10, 14), place));
faces.addAll(modelPartBox(texture.get(), 0, 0, new Vec3(left, 9, 1), new Vec3(width, 5, 14), place));
faces.addAll(modelPartBox(texture.get(), 0, 0, new Vec3(lockX, 7, 15), new Vec3(lockWidth, 4, 1), place));
return faces;
}
private static String chestTextureName(String blockName) {
String name = blockName.startsWith("waxed_") ? blockName.substring("waxed_".length()) : blockName;
return switch (name) {
case "trapped_chest" -> "trapped";
case "ender_chest" -> "ender";
case "copper_chest" -> "copper";
case "exposed_copper_chest" -> "copper_exposed";
case "weathered_copper_chest" -> "copper_weathered";
case "oxidized_copper_chest" -> "copper_oxidized";
default -> "normal";
};
}
// Heads are drawn upside down and mirrored in their model, and turn in sixteenths like signs.
private List<Face> head(BlockStateId state, ModelDefinition model) {
boolean onWall = state.name().contains("_wall_");
String kind = state.name().replace("_wall_", "_");
Optional<HeadLook> look = HeadLook.of(kind);
Optional<BlockTexture> texture = look.flatMap(found -> textures.entityTexture(found.texture(), MinecraftBlockTints.NO_TINT));
if (texture.isEmpty()) {
return onWall
? particleBox(state, model, new Vec3(4, 4, 8), new Vec3(12, 12, 16), false)
: particleBox(state, model, new Vec3(4, 0, 4), new Vec3(12, 8, 12), false);
}
Transform place;
if (onWall) {
CubeFace facing = CubeFace.named(state.properties().getOrDefault("facing", "north"));
place = Transform.IDENTITY
.translate(0.5D - facing.stepX() * SKULL_WALL_OFFSET, SKULL_WALL_OFFSET, 0.5D - facing.stepZ() * SKULL_WALL_OFFSET)
.scale(-1D, -1D, 1D)
.rotateY(yRotationOf(facing.opposite().modelName()));
} else {
int rotation = Integer.parseInt(state.properties().getOrDefault("rotation", "0"));
place = Transform.IDENTITY
.translate(0.5D, 0D, 0.5D)
.scale(-1D, -1D, 1D)
.rotateY(rotation * ROTATION_SEGMENT_DEGREES);
}
HeadLook found = look.get();
return modelPartBox(texture.get(), 0, 0, new Vec3(-found.width() / 2D, -8, -4), new Vec3(found.width(), 8, 8), place);
}
private List<Face> banner(BlockStateId state) {
boolean onWall = state.name().endsWith("_wall_banner");
String color = state.name().substring(0, state.name().length() - (onWall ? "_wall_banner" : "_banner").length());
Optional<BlockTexture> frame = textures.entityTexture("entity/banner/banner_base", MinecraftBlockTints.NO_TINT);
Optional<BlockTexture> cloth = textures.entityTexture("entity/banner/base", DYE_COLORS.getOrDefault(color, MinecraftBlockTints.NO_TINT));
if (frame.isEmpty() || cloth.isEmpty()) {
return List.of();
}
Transform place;
if (onWall) {
CubeFace facing = CubeFace.named(state.properties().getOrDefault("facing", "north"));
place = Transform.IDENTITY
.translate(0.5D, -1D / 6D, 0.5D)
.rotateY(-yRotationOf(facing.modelName()))
.translate(0D, -0.3125D, -0.4375D)
.scale(BANNER_SCALE, -BANNER_SCALE, -BANNER_SCALE);
} else {
int rotation = Integer.parseInt(state.properties().getOrDefault("rotation", "0"));
place = Transform.IDENTITY
.translate(0.5D, 0.5D, 0.5D)
.rotateY(-rotation * ROTATION_SEGMENT_DEGREES)
.scale(BANNER_SCALE, -BANNER_SCALE, -BANNER_SCALE);
}
List<Face> faces = new ArrayList<>();
if (!onWall) {
faces.addAll(modelPartBox(frame.get(), 44, 0, new Vec3(-1, -30, -1), new Vec3(2, 42, 2), place));
}
faces.addAll(modelPartBox(frame.get(), 0, 42, new Vec3(-10, -32, -1), new Vec3(20, 2, 2), place));
faces.addAll(modelPartBox(cloth.get(), 0, 0, new Vec3(-10, -32, -2), new Vec3(20, 40, 1), place));
return faces;
}
// Minecraft's facing angle: south is 0 and each quarter turn clockwise seen from above adds 90 degrees.
private static double yRotationOf(String facing) {
return switch (facing) {
case "west" -> 90D;
case "north" -> 180D;
case "east" -> 270D;
default -> 0D;
};
}
/**
* A box of a block entity's model, in model pixels, with its sides laid out on the texture the way Minecraft's
* model parts are: the top and bottom in a row above the four sides.
*/
static List<Face> modelPartBox(BlockTexture texture, int textureU, int textureV, Vec3 from, Vec3 size, Transform place) {
double minX = from.x() / MODEL_UNITS;
double minY = from.y() / MODEL_UNITS;
double minZ = from.z() / MODEL_UNITS;
double maxX = (from.x() + size.x()) / MODEL_UNITS;
double maxY = (from.y() + size.y()) / MODEL_UNITS;
double maxZ = (from.z() + size.z()) / MODEL_UNITS;
Vec3 v0 = new Vec3(minX, minY, minZ);
Vec3 v1 = new Vec3(maxX, minY, minZ);
Vec3 v2 = new Vec3(maxX, maxY, minZ);
Vec3 v3 = new Vec3(minX, maxY, minZ);
Vec3 v4 = new Vec3(minX, minY, maxZ);
Vec3 v5 = new Vec3(maxX, minY, maxZ);
Vec3 v6 = new Vec3(maxX, maxY, maxZ);
Vec3 v7 = new Vec3(minX, maxY, maxZ);
double u = textureU;
double v = textureV;
double width = size.x();
double height = size.y();
double depth = size.z();
List<Face> faces = new ArrayList<>(6);
faces.add(polygon(texture, place, CubeFace.DOWN, v5, v4, v0, u + depth, v, u + depth + width, v + depth));
faces.add(polygon(texture, place, CubeFace.UP, v2, v3, v7, u + depth + width, v + depth, u + depth + width + width, v));
faces.add(polygon(texture, place, CubeFace.WEST, v0, v4, v7, u, v + depth, u + depth, v + depth + height));
faces.add(polygon(texture, place, CubeFace.NORTH, v1, v0, v3, u + depth, v + depth, u + depth + width, v + depth + height));
faces.add(polygon(texture, place, CubeFace.EAST, v5, v1, v2, u + depth + width, v + depth, u + depth + width + depth, v + depth + height));
faces.add(polygon(texture, place, CubeFace.SOUTH, v4, v5, v6, u + depth + width + depth, v + depth, u + depth + width + depth + width, v + depth + height));
return faces;
}
// A model part's polygon gives its first corner (u2, v1), its second (u1, v1) and its third (u1, v2).
private static Face polygon(BlockTexture texture, Transform place, CubeFace side, Vec3 first, Vec3 second, Vec3 third,
double u1, double v1, double u2, double v2) {
double textureWidth = texture.width();
double textureHeight = texture.height();
TextureMapping mapping = new TextureMapping(u1 / textureWidth, v1 / textureHeight,
(u2 - u1) / textureWidth, 0D, 0D, (v2 - v1) / textureHeight);
Face face = new Face(second, first.minus(second), third.minus(second), side.direction(), texture, mapping, side.shade(), null)
.transformed(place);
return face.withShade(CubeFace.nearest(face.normal()).shade());
}
/**
* The entity texture of a mob head and how wide its head is.
*/
private record HeadLook(String texture, double width) {
private static final double HEAD_WIDTH = 8D;
private static final double PIGLIN_HEAD_WIDTH = 10D;
static Optional<HeadLook> of(String kind) {
return Optional.ofNullable(switch (kind) {
case "skeleton_skull" -> new HeadLook("entity/skeleton/skeleton", HEAD_WIDTH);
case "wither_skeleton_skull" -> new HeadLook("entity/skeleton/wither_skeleton", HEAD_WIDTH);
case "zombie_head" -> new HeadLook("entity/zombie/zombie", HEAD_WIDTH);
case "creeper_head" -> new HeadLook("entity/creeper/creeper", HEAD_WIDTH);
case "player_head" -> new HeadLook("entity/player/wide/steve", HEAD_WIDTH);
case "piglin_head" -> new HeadLook("entity/piglin/piglin", PIGLIN_HEAD_WIDTH);
default -> null;
});
}
}
}
@@ -0,0 +1,224 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.Iris;
import com.volmit.iris.core.events.IrisEngineHotloadEvent;
import com.volmit.iris.core.project.IrisProject;
import com.volmit.iris.core.service.PreservationSVC;
import com.volmit.iris.core.service.StudioSVC;
import com.volmit.iris.core.tools.IrisToolbelt;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.platform.PlatformChunkGenerator;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import javax.swing.JFrame;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.desktop.QuitResponse;
import java.awt.image.BufferedImage;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
public final class GuiHost {
private static final AtomicBoolean DESKTOP_QUIT_GUARD_INSTALLED = new AtomicBoolean(false);
private static final Set<JFrame> MANAGED_FRAMES = ConcurrentHashMap.newKeySet();
private static final Map<Runnable, Listener> HOTLOAD_HOOKS = new ConcurrentHashMap<>();
private GuiHost() {
}
public static boolean isAvailable() {
return !GraphicsEnvironment.isHeadless();
}
public static JFrame createFrame(String title) {
XwaylandWindowManagerHint.apply();
StudioTheme.install();
JFrame frame = new JFrame(title);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
MANAGED_FRAMES.add(frame);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent event) {
MANAGED_FRAMES.remove(frame);
}
@Override
public void windowActivated(WindowEvent event) {
StudioTheme.followSystem();
}
});
installMacQuitGuard();
return frame;
}
/**
* Wraps rendered pixels in an image Java2D draws straight from memory. Left to itself Java2D copies every image it
* draws into a texture inside the display server; with a map's hundreds of tiles that crashed Xwayland's Intel driver
* and took every X window on the desktop down with it.
*/
public static BufferedImage uncachedImage(int[] pixels, int width, int height) {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
image.getRaster().setDataElements(0, 0, width, height, pixels);
image.setAccelerationPriority(0F);
return image;
}
// A scaled-up default size can be bigger than a small or low resolution screen.
static Dimension fitToScreen(Dimension preferred) {
Rectangle usable = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
return new Dimension(Math.min(preferred.width, usable.width), Math.min(preferred.height, usable.height));
}
/**
* The engine of the open studio project, which reads the pack folder you edit, or else the first Iris world.
* A normal Iris world runs on its own copy of the pack, made when the world was created.
*/
public static Engine findActiveEngine() {
Engine studioEngine = findStudioEngine();
if (studioEngine != null) {
return studioEngine;
}
try {
for (World world : new ArrayList<>(Bukkit.getWorlds())) {
Engine engine = engineOf(world);
if (engine != null) {
return engine;
}
}
} catch (Throwable ignored) {
}
return null;
}
private static Engine findStudioEngine() {
try {
StudioSVC studio = Iris.service(StudioSVC.class);
IrisProject project = studio == null || !studio.isProjectOpen() ? null : studio.getActiveProject();
PlatformChunkGenerator provider = project == null ? null : project.getActiveProvider();
Engine engine = provider == null ? null : provider.getEngine();
return engine == null || engine.isClosed() ? null : engine;
} catch (Throwable ignored) {
return null;
}
}
public static Engine findEngine(UUID worldId) {
World world = Bukkit.getWorld(worldId);
return world == null ? null : engineOf(world);
}
public static void registerHotloadHook(Runnable onHotload) {
Listener listener = new HotloadListener(onHotload);
HOTLOAD_HOOKS.put(onHotload, listener);
Iris.instance.registerListener(listener);
}
public static void unregisterHotloadHook(Runnable onHotload) {
Listener listener = HOTLOAD_HOOKS.remove(onHotload);
if (listener != null) {
Iris.instance.unregisterListener(listener);
}
}
static void interruptOnPluginShutdown(ExecutorService executor) {
PreservationSVC preservation = Iris.service(PreservationSVC.class);
if (preservation != null) {
preservation.register(executor);
}
}
static void interruptOnPluginShutdown(Thread thread) {
PreservationSVC preservation = Iris.service(PreservationSVC.class);
if (preservation != null) {
preservation.register(thread);
}
}
private static Engine engineOf(World world) {
try {
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
return access.getEngine();
}
} catch (Throwable ignored) {
}
return null;
}
// macOS Quit in the dock menu would otherwise stop the whole server.
private static void installMacQuitGuard() {
if (!isAvailable()
|| !System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("mac")
|| !DESKTOP_QUIT_GUARD_INSTALLED.compareAndSet(false, true)) {
return;
}
try {
if (!Desktop.isDesktopSupported()) {
return;
}
Desktop desktop = Desktop.getDesktop();
if (!desktop.isSupported(Desktop.Action.APP_QUIT_HANDLER)) {
return;
}
desktop.setQuitHandler((event, response) -> closeWindowsAndCancelQuit(response));
} catch (Throwable e) {
Iris.reportError(e);
Iris.info("Unable to install the Iris desktop quit guard; use the server stop command instead of macOS Quit");
}
}
private static void closeWindowsAndCancelQuit(QuitResponse response) {
response.cancelQuit();
EventQueue.invokeLater(() -> {
for (JFrame frame : MANAGED_FRAMES) {
frame.dispose();
}
});
}
private static final class HotloadListener implements Listener {
private final Runnable onHotload;
private HotloadListener(Runnable onHotload) {
this.onHotload = onHotload;
}
@EventHandler
public void on(IrisEngineHotloadEvent event) {
onHotload.run();
}
}
}
@@ -0,0 +1,29 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
public record GuiMarker(String label, double worldX, double worldY, double worldZ, double health, double maxHealth) {
public static GuiMarker player(String name, double worldX, double worldZ) {
return new GuiMarker(name, worldX, 0, worldZ, 0, 0);
}
public static GuiMarker entity(String label, double worldX, double worldY, double worldZ, double health, double maxHealth) {
return new GuiMarker(label, worldX, worldY, worldZ, health, maxHealth);
}
}
@@ -0,0 +1,105 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import java.awt.Cursor;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
/**
* Lays an island over content that fills the whole area behind it. Dragging the island's right edge resizes it,
* always leaving the content some room beside it.
*/
final class IslandOverlay extends JLayeredPane {
private static final long serialVersionUID = -4182209135360718342L;
private final IslandPanel island;
private final JComponent background;
private final int minimumIslandWidth;
private final int minimumUncoveredWidth;
private int islandWidth;
IslandOverlay(IslandPanel island, JComponent background, int islandWidth, int minimumIslandWidth, int minimumUncoveredWidth) {
this.island = island;
this.background = background;
this.islandWidth = islandWidth;
this.minimumIslandWidth = minimumIslandWidth;
this.minimumUncoveredWidth = minimumUncoveredWidth;
add(background, JLayeredPane.DEFAULT_LAYER);
add(island, JLayeredPane.PALETTE_LAYER);
installResizing();
}
@Override
public void doLayout() {
background.setBounds(0, 0, getWidth(), getHeight());
island.setBounds(0, 0, clampedIslandWidth(), getHeight());
}
private int clampedIslandWidth() {
int widest = Math.max(minimumIslandWidth, getWidth() - minimumUncoveredWidth);
return Math.clamp(islandWidth, minimumIslandWidth, widest);
}
private void installResizing() {
MouseAdapter resizer = new MouseAdapter() {
private boolean dragging;
@Override
public void mouseMoved(MouseEvent event) {
island.setCursor(island.isOnResizeEdge(event.getX(), event.getY())
? Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR)
: Cursor.getDefaultCursor());
}
@Override
public void mouseExited(MouseEvent event) {
if (!dragging) {
island.setCursor(Cursor.getDefaultCursor());
}
}
@Override
public void mousePressed(MouseEvent event) {
dragging = island.isOnResizeEdge(event.getX(), event.getY());
}
@Override
public void mouseDragged(MouseEvent event) {
if (!dragging) {
return;
}
islandWidth = event.getX();
islandWidth = clampedIslandWidth();
revalidate();
repaint();
}
@Override
public void mouseReleased(MouseEvent event) {
dragging = false;
}
};
island.addMouseListener(resizer);
island.addMouseMotionListener(resizer);
}
}
@@ -0,0 +1,77 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import javax.swing.JComponent;
import javax.swing.JPanel;
import java.awt.BorderLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.RoundRectangle2D;
/**
* A rounded panel floating over the window content with a gap all around it, like the tool windows of IntelliJ's
* islands theme. The gap belongs to the content behind it, except for the strip along the right edge, which grabs the
* island to resize it.
*/
final class IslandPanel extends JPanel {
private static final long serialVersionUID = 7313084027614358519L;
private static final int GAP = 6;
private static final int CORNER_ARC = 12;
private static final int INNER_PADDING = 4;
IslandPanel(JComponent content) {
super(new BorderLayout());
setOpaque(false);
int inset = GAP + INNER_PADDING;
setBorder(StudioTheme.emptyBorder(inset, inset, inset, inset));
add(content, BorderLayout.CENTER);
}
boolean isOnResizeEdge(int x, int y) {
int edgeWidth = StudioTheme.scale(GAP + INNER_PADDING);
return x >= getWidth() - edgeWidth && x < getWidth() && y >= 0 && y < getHeight();
}
// Clicks in the gap reach the content behind the island instead of the island itself.
@Override
public boolean contains(int x, int y) {
return island().contains(x, y) || isOnResizeEdge(x, y);
}
@Override
protected void paintComponent(Graphics graphics) {
Graphics2D canvas = (Graphics2D) graphics.create();
try {
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
canvas.setColor(StudioTheme.PANEL_BACKGROUND);
canvas.fill(island());
} finally {
canvas.dispose();
}
}
private RoundRectangle2D island() {
double gap = StudioTheme.scale(GAP);
double arc = StudioTheme.scale(CORNER_ARC);
return new RoundRectangle2D.Double(gap, gap, getWidth() - gap * 2D, getHeight() - gap * 2D, arc, arc);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,183 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.core.gui.components.MapLabels;
import com.volmit.iris.core.gui.components.MapStyle;
import com.volmit.iris.core.gui.components.MapTile;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.ToIntFunction;
/**
* Measures the visible part of the map from the tiles already drawn: how much each biome or region covers, and which
* heights it spans.
*/
final class MapCensus {
private static final long MAXIMUM_COUNTED_PIXELS = 250_000L;
private static final int MINIMUM_HEIGHT_SAMPLES = 64;
private static final double LOW_HEIGHT_PERCENTILE = 0.01D;
private static final double HIGH_HEIGHT_PERCENTILE = 0.99D;
private static final double HEIGHT_RANGE_STEP_BLOCKS = 8D;
private static final double MINIMUM_WATER_DEPTH_BLOCKS = 8D;
private static final double MINIMUM_LAND_HEIGHT_BLOCKS = 16D;
private MapCensus() {
}
static List<Share> count(VisionRenderController.Frame frame, ViewArea area, ToIntFunction<MapLabels.Label> colorOf) {
Map<Short, long[]> counts = new HashMap<>();
MapLabels[] labels = new MapLabels[1];
long counted = visitVisiblePixels(frame, area, (tile, pixelX, pixelZ) -> {
labels[0] = tile.labels();
counts.computeIfAbsent(tile.subjectAt(pixelX, pixelZ), id -> new long[1])[0]++;
});
return shares(counts, labels[0], counted, colorOf);
}
/**
* The heights on screen, ignoring the highest and lowest few percent so one spike doesn't wash out every colour.
* Rounded to whole steps, so panning around doesn't recolour the map for every small change.
*
* @return the range, or null until enough of the map is drawn to tell
*/
static MapStyle.HeightRange heightRange(VisionRenderController.Frame frame, ViewArea area, double seaLevel) {
DoubleList heights = new DoubleList();
visitVisiblePixels(frame, area, (tile, pixelX, pixelZ) -> {
if (tile.hasHeights()) {
heights.add(tile.heightAt(pixelX, pixelZ));
}
});
if (heights.size() < MINIMUM_HEIGHT_SAMPLES) {
return null;
}
double[] sorted = heights.sorted();
double low = sorted[(int) ((sorted.length - 1) * LOW_HEIGHT_PERCENTILE)];
double high = sorted[(int) ((sorted.length - 1) * HIGH_HEIGHT_PERCENTILE)];
double lowest = Math.floor(Math.min(low, seaLevel - MINIMUM_WATER_DEPTH_BLOCKS) / HEIGHT_RANGE_STEP_BLOCKS)
* HEIGHT_RANGE_STEP_BLOCKS;
double highest = Math.ceil(Math.max(high, seaLevel + MINIMUM_LAND_HEIGHT_BLOCKS) / HEIGHT_RANGE_STEP_BLOCKS)
* HEIGHT_RANGE_STEP_BLOCKS;
return new MapStyle.HeightRange(lowest, seaLevel, highest);
}
private static long visitVisiblePixels(VisionRenderController.Frame frame, ViewArea area, PixelVisitor visitor) {
if (frame == null) {
return 0L;
}
double blocksPerPixel = frame.renderBlocksPerPixel();
int stride = countingStride(area.maximumX() - area.minimumX(), area.maximumZ() - area.minimumZ(), blocksPerPixel);
double tileSpan = VisionRenderController.TILE_PIXELS * blocksPerPixel;
long visited = 0L;
for (VisionRenderController.VisibleTile visible : frame.tiles()) {
MapTile tile = visible.tile();
if (tile == null) {
continue;
}
double originX = visible.tileX() * tileSpan;
double originZ = visible.tileZ() * tileSpan;
int firstX = firstPixel(area.minimumX() - originX, blocksPerPixel, stride, visible.tileX());
int firstZ = firstPixel(area.minimumZ() - originZ, blocksPerPixel, stride, visible.tileZ());
int endX = endPixel(area.maximumX() - originX, blocksPerPixel, tile.size());
int endZ = endPixel(area.maximumZ() - originZ, blocksPerPixel, tile.size());
for (int pixelZ = firstZ; pixelZ < endZ; pixelZ += stride) {
for (int pixelX = firstX; pixelX < endX; pixelX += stride) {
visitor.visit(tile, pixelX, pixelZ);
visited++;
}
}
}
return visited;
}
private static List<Share> shares(Map<Short, long[]> counts, MapLabels labels, long counted,
ToIntFunction<MapLabels.Label> colorOf) {
if (labels == null || counted == 0L) {
return List.of();
}
List<Share> shares = new ArrayList<>(counts.size());
for (Map.Entry<Short, long[]> entry : counts.entrySet()) {
MapLabels.Label label = labels.label(entry.getKey());
if (label != null) {
shares.add(new Share(label, colorOf.applyAsInt(label), entry.getValue()[0] / (double) counted));
}
}
shares.sort(Comparator.comparingDouble(Share::fraction).reversed().thenComparing(share -> share.label().name()));
return List.copyOf(shares);
}
// Counting every pixel of a big screen is wasted work; an evenly spread sample gives the same percentages.
private static int countingStride(double widthBlocks, double heightBlocks, double blocksPerPixel) {
double pixels = (widthBlocks / blocksPerPixel) * (heightBlocks / blocksPerPixel);
return Math.max(1, (int) Math.ceil(Math.sqrt(pixels / MAXIMUM_COUNTED_PIXELS)));
}
// Starting on the global stride grid keeps neighbouring tiles from sampling the same columns twice as densely.
private static int firstPixel(double offsetBlocks, double blocksPerPixel, int stride, long tileIndex) {
int first = Math.max(0, (int) Math.ceil(offsetBlocks / blocksPerPixel));
long globalPixel = tileIndex * VisionRenderController.TILE_PIXELS + first;
int misalignment = (int) Math.floorMod(globalPixel, (long) stride);
return misalignment == 0 ? first : first + stride - misalignment;
}
private static int endPixel(double offsetBlocks, double blocksPerPixel, int size) {
return Math.min(size, (int) Math.ceil(offsetBlocks / blocksPerPixel));
}
record Share(MapLabels.Label label, int color, double fraction) {
}
/**
* The world area the screen shows, in blocks.
*/
record ViewArea(double minimumX, double minimumZ, double maximumX, double maximumZ) {
}
@FunctionalInterface
private interface PixelVisitor {
void visit(MapTile tile, int pixelX, int pixelZ);
}
private static final class DoubleList {
private double[] values = new double[1024];
private int size;
void add(double value) {
if (size == values.length) {
values = Arrays.copyOf(values, size * 2);
}
values[size++] = value;
}
int size() {
return size;
}
double[] sorted() {
double[] sorted = Arrays.copyOf(values, size);
Arrays.sort(sorted);
return sorted;
}
}
}
@@ -0,0 +1,112 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.core.gui.components.MapLabels;
import com.volmit.iris.core.gui.components.RenderType;
import com.volmit.iris.core.loader.IrisRegistrant;
import com.volmit.iris.engine.object.IrisBiome;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* The text of the card that follows the cursor: what the drawn map shows there at once, and the rest once probed.
*/
final class MapHoverCard {
private MapHoverCard() {
}
/**
* @param subject the biome, or region in the region view, the drawn tile shows; null while that tile is drawing
* @param details the probed facts about the spot, or null until they arrive
* @param detailed whether Shift is held, which adds keys and files
*/
static List<String> lines(MapLabels.Label subject, MapLabels.Label region, MapHoverProbe.Details details,
RenderType type, boolean detailed, double worldX, double worldZ) {
List<String> lines = new ArrayList<>();
lines.add(title(subject, details));
if (type != RenderType.REGION) {
String regionName = region != null ? region.name() : details != null ? details.region().getName() : null;
if (regionName != null) {
lines.add("Region " + regionName);
}
}
if (details != null) {
addTerrain(lines, details, type);
}
int blockX = MapCanvas.floorWorldCoordinate(worldX);
int blockZ = MapCanvas.floorWorldCoordinate(worldZ);
lines.add("Block " + blockX + ", " + blockZ + " · chunk " + (blockX >> 4) + ", " + (blockZ >> 4));
if (!detailed) {
lines.add("Shift for keys and files · H highlights");
return lines;
}
addSource(lines, subject);
if (subject != null && subject.kind() != MapLabels.Kind.REGION) {
addSource(lines, region);
}
lines.add("Region file r." + (blockX >> 9) + "." + (blockZ >> 9) + ".mca");
return lines;
}
private static String title(MapLabels.Label subject, MapHoverProbe.Details details) {
if (subject != null) {
return subject.name();
}
if (details != null && details.surfaceBiome() != null) {
return details.surfaceBiome().getName();
}
return "Drawing…";
}
private static void addTerrain(List<String> lines, MapHoverProbe.Details details, RenderType type) {
lines.add("Height " + Math.round(details.height()) + " · sea level " + Math.round(details.fluidHeight()));
String slope = "slope " + Math.round(details.slopeDegrees()) + "°";
lines.add(details.terrainKind() == null ? capitalize(slope) : details.terrainKind() + " · " + slope);
IrisBiome surface = details.surfaceBiome();
IrisBiome cave = details.caveBiome();
if (type == RenderType.CAVE_LAND && surface != null) {
lines.add("Surface biome " + surface.getName());
} else if (type != RenderType.CAVE_LAND && cave != null && cave != surface) {
lines.add("Cave biome " + cave.getName());
}
if (details.vanillaBiome() != null) {
lines.add("Minecraft biome " + details.vanillaBiome().replace('_', ' '));
}
}
private static void addSource(List<String> lines, MapLabels.Label label) {
if (label == null || !(label.subject() instanceof IrisRegistrant registrant)) {
return;
}
String kind = label.kind() == MapLabels.Kind.REGION ? "Region" : "Biome";
lines.add(kind + " key " + label.key());
File file = registrant.getLoadFile();
if (file != null) {
File folder = file.getParentFile();
lines.add(kind + " file " + (folder == null ? "" : folder.getName() + "/") + file.getName());
}
}
private static String capitalize(String text) {
return Character.toUpperCase(text.charAt(0)) + text.substring(1);
}
}
@@ -0,0 +1,90 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.engine.IrisComplex;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.InferredType;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.engine.object.IrisDimension;
import com.volmit.iris.engine.object.IrisRegion;
import org.bukkit.block.Biome;
import java.util.Locale;
/**
* Looks up everything the hover card can tell about one spot of the world; runs off the Swing thread.
*/
final class MapHoverProbe {
// IrisComplex measures slope as the height change over this many blocks.
private static final double SLOPE_STREAM_RANGE_BLOCKS = 3D;
private MapHoverProbe() {
}
static Details sample(Engine engine, double worldX, double worldZ) {
IrisComplex complex = engine.getComplex();
IrisBiome surfaceBiome = complex.getBaseBiomeStream().get(worldX, worldZ);
IrisBiome caveBiome = complex.getCaveBiomeStream().get(worldX, worldZ);
IrisRegion region = complex.getRegionStream().get(worldX, worldZ);
InferredType terrainKind = complex.getBridgeStream().get(worldX, worldZ);
Biome vanillaBiome = complex.getTrueBiomeDerivativeStream().get(worldX, worldZ);
double height = complex.getHeightStream().getDouble(worldX, worldZ);
double slope = complex.getSlopeStream().getDouble(worldX, worldZ);
IrisDimension dimension = engine.getDimension();
return new Details(
worldX,
worldZ,
surfaceBiome,
caveBiome,
region,
terrainKindName(surfaceBiome, terrainKind),
vanillaBiome == null ? null : vanillaBiome.getKey().getKey(),
height,
dimension == null ? 0D : dimension.getFluidHeight(),
Math.toDegrees(Math.atan(slope / SLOPE_STREAM_RANGE_BLOCKS))
);
}
// A shore biome sits on land the continent stream calls land or sea, so the biome's own type says it best.
private static String terrainKindName(IrisBiome surfaceBiome, InferredType continentKind) {
InferredType kind = surfaceBiome != null && surfaceBiome.getInferredType() != null
? surfaceBiome.getInferredType()
: continentKind;
if (kind == null) {
return null;
}
String name = kind.name().toLowerCase(Locale.ROOT);
return Character.toUpperCase(name.charAt(0)) + name.substring(1);
}
record Details(
double worldX,
double worldZ,
IrisBiome surfaceBiome,
IrisBiome caveBiome,
IrisRegion region,
String terrainKind,
String vanillaBiome,
double height,
double fluidHeight,
double slopeDegrees
) {
}
}
@@ -0,0 +1,230 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.core.gui.components.MapStyle;
import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.ListSelectionModel;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.function.Consumer;
/**
* The biomes or regions on screen with how much of it each covers; clicking one highlights it on the map.
*/
final class MapLegendPanel extends JPanel {
private static final long serialVersionUID = 4471297416386034128L;
private static final int PANEL_WIDTH = 250;
private static final int ROW_HEIGHT = 26;
private static final int SWATCH_SIZE = 12;
private static final int SWATCH_GAP = 8;
private static final int ROW_PADDING = 10;
private static final int PERCENT_WIDTH = 42;
private static final String DRAWING_MESSAGE = "Drawing the map…";
private final JLabel title = new JLabel();
private final DefaultListModel<MapCensus.Share> rows = new DefaultListModel<>();
private final JList<MapCensus.Share> list = new JList<>(rows) {
// Long names shorten with an ellipsis instead of pushing the percentages out of sight.
@Override
public boolean getScrollableTracksViewportWidth() {
return true;
}
};
private final Set<String> sharedNames = new HashSet<>();
private MapStyle.Highlight highlight;
MapLegendPanel(Consumer<MapCensus.Share> onRowClicked) {
super(new BorderLayout());
setPreferredSize(StudioTheme.scaledSize(PANEL_WIDTH, 0));
setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, StudioTheme.SEPARATOR));
setBackground(StudioTheme.PANEL_BACKGROUND);
title.setFont(StudioTheme.sectionFont());
title.setForeground(StudioTheme.MUTED_TEXT);
title.setBorder(StudioTheme.emptyBorder(10, 16, 6, 16));
add(title, BorderLayout.NORTH);
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setFocusable(false);
list.setFixedCellHeight(StudioTheme.scale(ROW_HEIGHT));
list.setCellRenderer(new ShareRenderer());
list.setBackground(StudioTheme.PANEL_BACKGROUND);
list.setBorder(StudioTheme.emptyBorder(0, 6, 4, 6));
list.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent event) {
MapCensus.Share share = shareAt(event.getPoint());
if (share != null) {
onRowClicked.accept(share);
}
}
});
add(StudioToolbar.scroll(list), BorderLayout.CENTER);
JLabel hint = new JLabel("Click to highlight · Esc clears");
hint.setFont(StudioTheme.smallFont());
hint.setForeground(StudioTheme.MUTED_TEXT);
hint.setBorder(StudioTheme.emptyBorder(6, 16, 8, 16));
add(hint, BorderLayout.SOUTH);
}
void show(String subjectTitle, List<MapCensus.Share> shares, MapStyle.Highlight nextHighlight) {
title.setText(subjectTitle.toUpperCase(Locale.ROOT));
highlight = nextHighlight;
rememberSharedNames(shares);
if (!sameRows(shares)) {
rows.clear();
rows.addAll(shares);
}
list.repaint();
}
// Two biomes can share a display name; those rows show their key so they can be told apart.
private void rememberSharedNames(List<MapCensus.Share> shares) {
Set<String> seen = new HashSet<>();
sharedNames.clear();
for (MapCensus.Share share : shares) {
if (!seen.add(share.label().name())) {
sharedNames.add(share.label().name());
}
}
}
private String rowText(MapCensus.Share share) {
String name = share.label().name();
return sharedNames.contains(name) ? name + " (" + share.label().key() + ")" : name;
}
private boolean sameRows(List<MapCensus.Share> shares) {
if (rows.size() != shares.size()) {
return false;
}
for (int index = 0; index < shares.size(); index++) {
if (!rows.get(index).equals(shares.get(index))) {
return false;
}
}
return true;
}
private MapCensus.Share shareAt(Point point) {
int index = list.locationToIndex(point);
if (index < 0) {
return null;
}
Rectangle bounds = list.getCellBounds(index, index);
return bounds != null && bounds.contains(point) ? rows.get(index) : null;
}
private boolean isHighlighted(MapCensus.Share share) {
return highlight != null
&& highlight.kind() == share.label().kind()
&& highlight.key().equals(share.label().key());
}
@Override
protected void paintChildren(Graphics graphics) {
super.paintChildren(graphics);
if (!rows.isEmpty()) {
return;
}
Graphics2D canvas = (Graphics2D) graphics.create();
try {
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
canvas.setFont(StudioTheme.font());
canvas.setColor(StudioTheme.MUTED_TEXT);
canvas.drawString(DRAWING_MESSAGE, StudioTheme.scale(16),
title.getHeight() + StudioTheme.scale(4) + canvas.getFontMetrics().getAscent());
} finally {
canvas.dispose();
}
}
private final class ShareRenderer extends DefaultListCellRenderer {
private static final long serialVersionUID = -2350212934617052618L;
private Color swatch = Color.BLACK;
private String percent = "";
@Override
public Component getListCellRendererComponent(JList<?> owner, Object value, int index, boolean selected, boolean focused) {
MapCensus.Share share = (MapCensus.Share) value;
super.getListCellRendererComponent(owner, rowText(share), index, isHighlighted(share), false);
setFont(StudioTheme.font());
setBorder(StudioTheme.emptyBorder(0, ROW_PADDING + SWATCH_SIZE + SWATCH_GAP, 0, ROW_PADDING + PERCENT_WIDTH));
setToolTipText(share.label().key());
swatch = new Color(share.color());
percent = formatPercent(share.fraction());
return this;
}
@Override
protected void paintComponent(Graphics graphics) {
super.paintComponent(graphics);
Graphics2D canvas = (Graphics2D) graphics.create();
try {
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
int size = StudioTheme.scale(SWATCH_SIZE);
int swatchX = StudioTheme.scale(ROW_PADDING);
int swatchY = (getHeight() - size) / 2;
int arc = StudioTheme.scale(4);
canvas.setColor(swatch);
canvas.fillRoundRect(swatchX, swatchY, size, size, arc, arc);
canvas.setColor(StudioTheme.CONTROL_BORDER);
canvas.drawRoundRect(swatchX, swatchY, size - 1, size - 1, arc, arc);
canvas.setFont(StudioTheme.smallFont());
canvas.setColor(StudioTheme.SECONDARY_TEXT);
int textWidth = canvas.getFontMetrics().stringWidth(percent);
int baseline = (getHeight() + canvas.getFontMetrics().getAscent() - canvas.getFontMetrics().getDescent()) / 2;
canvas.drawString(percent, getWidth() - StudioTheme.scale(ROW_PADDING) - textWidth, baseline);
} finally {
canvas.dispose();
}
}
}
private static String formatPercent(double fraction) {
double percent = fraction * 100D;
if (percent < 0.1D) {
return "<0.1%";
}
return percent < 10D
? String.format(Locale.ROOT, "%.1f%%", percent)
: String.format(Locale.ROOT, "%.0f%%", percent);
}
}
@@ -0,0 +1,116 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.core.gui.components.RenderType;
import java.awt.event.KeyEvent;
/**
* Everything the map window can show, in the order and groups of its view list.
*/
enum MapView {
BIOMES("Biomes", "The biome at each spot, in the biome's colour", Group.WORLD, RenderType.BIOME, KeyEvent.VK_1),
REGIONS("Regions", "The region each spot belongs to", Group.WORLD, RenderType.REGION, KeyEvent.VK_2),
HEIGHT("Height", "Terrain height, from deep water to snowy peaks", Group.WORLD, RenderType.HEIGHT, KeyEvent.VK_3),
CONTINENTS("Continents", "Land, coast or sea, judged by each biome's first height generator",
Group.WORLD, RenderType.CONTINENT, KeyEvent.VK_4),
LAND_BIOMES("Land biomes", "The land biome each spot would get, also where the sea is",
Group.BIOME_STREAMS, RenderType.BIOME_LAND, KeyEvent.VK_5),
SEA_BIOMES("Sea biomes", "The sea biome each spot would get, also where the land is",
Group.BIOME_STREAMS, RenderType.BIOME_SEA, KeyEvent.VK_6),
CAVE_BIOMES("Cave biomes", "The cave biome under each spot", Group.BIOME_STREAMS, RenderType.CAVE_LAND, KeyEvent.VK_7),
PALETTES("Palettes", "The blocks each biome's palette layers place, with their textures",
Group.BLOCKS, null, KeyEvent.VK_P),
OBJECTS("Objects", "Every object in the pack in 3D, with the blocks it is made of",
Group.BLOCKS, null, KeyEvent.VK_O),
NOISE("Noise", "The pack's noise generators and Iris' built-in noise styles",
Group.GENERATORS, null, KeyEvent.VK_N);
private final String label;
private final String description;
private final Group group;
private final RenderType renderType;
private final int shortcutKey;
MapView(String label, String description, Group group, RenderType renderType, int shortcutKey) {
this.label = label;
this.description = description;
this.group = group;
this.renderType = renderType;
this.shortcutKey = shortcutKey;
}
String label() {
return label;
}
String description() {
return description;
}
Group group() {
return group;
}
/**
* The tile renderer's mode for this view, or null for the palette, object and noise views, which draw themselves.
*/
RenderType renderType() {
return renderType;
}
int shortcutKey() {
return shortcutKey;
}
String shortcutText() {
return KeyEvent.getKeyText(shortcutKey);
}
boolean isTileMap() {
return renderType != null;
}
static MapView forShortcut(int keyCode) {
for (MapView view : values()) {
if (view.shortcutKey == keyCode) {
return view;
}
}
return null;
}
enum Group {
WORLD("World"),
BIOME_STREAMS("Biome layers"),
BLOCKS("Blocks"),
GENERATORS("Generators");
private final String label;
Group(String label) {
this.label = label;
}
String label() {
return label;
}
}
}
@@ -0,0 +1,47 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.formdev.flatlaf.extras.FlatSVGIcon;
import javax.swing.Icon;
import java.util.EnumMap;
import java.util.Locale;
import java.util.Map;
/**
* The icon of each map view, taken from IntelliJ's new UI icon set (see the NOTICE next to the files).
*/
final class MapViewIcon {
private static final String ICON_FOLDER = "iris/icons/views/";
private static final int SIZE = 16;
private static final Map<MapView, Icon> LOADED = new EnumMap<>(MapView.class);
private MapViewIcon() {
}
static Icon of(MapView view) {
return LOADED.computeIfAbsent(view, MapViewIcon::load);
}
private static Icon load(MapView view) {
String fileName = view.name().toLowerCase(Locale.ROOT).replace('_', '-') + ".svg";
return new FlatSVGIcon(ICON_FOLDER + fileName, SIZE, SIZE, MapViewIcon.class.getClassLoader());
}
}
@@ -0,0 +1,246 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JList;
import javax.swing.ListSelectionModel;
import javax.swing.UIManager;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Locale;
import java.util.function.Consumer;
/**
* The map window's list of views, grouped under headers, each row with a picture of the view and the key that opens
* it.
*/
final class MapViewList extends JList<MapViewList.Row> {
private static final long serialVersionUID = -4130872409914437415L;
private static final int ROW_HEIGHT = 28;
private static final int NO_ROW = -1;
private final DefaultListModel<Row> rows = new DefaultListModel<>();
private int lastViewRow;
private int hoveredRow = NO_ROW;
MapViewList(Consumer<MapView> onViewChosen) {
fillRows();
setModel(rows);
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
setFixedCellHeight(StudioTheme.scale(ROW_HEIGHT));
setCellRenderer(new RowRenderer());
setBorder(StudioTheme.emptyBorder(4, 6, 4, 6));
addListSelectionListener(event -> {
if (event.getValueIsAdjusting() || skipHeaderRow()) {
return;
}
Row selected = getSelectedValue();
if (selected == null) {
return;
}
lastViewRow = getSelectedIndex();
onViewChosen.accept(selected.view());
});
MouseAdapter hover = new MouseAdapter() {
@Override
public void mouseMoved(MouseEvent event) {
hover(viewRowAt(event));
}
@Override
public void mouseExited(MouseEvent event) {
hover(NO_ROW);
}
};
addMouseListener(hover);
addMouseMotionListener(hover);
}
private int viewRowAt(MouseEvent event) {
int row = locationToIndex(event.getPoint());
if (row < 0 || rows.get(row).isHeader() || !getCellBounds(row, row).contains(event.getPoint())) {
return NO_ROW;
}
return row;
}
private void hover(int row) {
if (row == hoveredRow) {
return;
}
repaintRow(hoveredRow);
hoveredRow = row;
repaintRow(hoveredRow);
}
private void repaintRow(int row) {
Rectangle bounds = row == NO_ROW ? null : getCellBounds(row, row);
if (bounds != null) {
repaint(bounds);
}
}
void select(MapView view) {
for (int row = 0; row < rows.size(); row++) {
if (rows.get(row).view() == view) {
lastViewRow = row;
setSelectedIndex(row);
ensureIndexIsVisible(row);
return;
}
}
}
private void fillRows() {
MapView.Group currentGroup = null;
for (MapView view : MapView.values()) {
if (view.group() != currentGroup) {
currentGroup = view.group();
rows.addElement(Row.header(currentGroup));
}
rows.addElement(Row.of(view));
}
}
// Headers can't be chosen: clicking one keeps the current view, and the arrow keys carry on past it.
private boolean skipHeaderRow() {
int row = getSelectedIndex();
if (row < 0 || !rows.get(row).isHeader()) {
return false;
}
int step = row >= lastViewRow ? 1 : -1;
int next = nextViewRow(row, step);
if (next < 0) {
next = nextViewRow(row, -step);
}
setSelectedIndex(next < 0 ? lastViewRow : next);
ensureIndexIsVisible(getSelectedIndex());
return true;
}
private int nextViewRow(int from, int step) {
for (int row = from + step; row >= 0 && row < rows.size(); row += step) {
if (!rows.get(row).isHeader()) {
return row;
}
}
return -1;
}
record Row(MapView.Group group, MapView view) {
static Row header(MapView.Group group) {
return new Row(group, null);
}
static Row of(MapView view) {
return new Row(view.group(), view);
}
boolean isHeader() {
return view == null;
}
}
private static final class RowRenderer extends DefaultListCellRenderer {
private static final long serialVersionUID = 6302156810211578474L;
private static final int ICON_TEXT_GAP = 8;
private static final int KEYCAP_SIZE = 17;
private static final int KEYCAP_ARC = 5;
private String shortcut;
private boolean hovered;
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean selected, boolean focused) {
Row row = (Row) value;
super.getListCellRendererComponent(list, row.isHeader() ? "" : row.view().label(), index, selected && !row.isHeader(), false);
if (row.isHeader()) {
setText(row.group().label().toUpperCase(Locale.ROOT));
setFont(StudioTheme.sectionFont());
setForeground(StudioTheme.MUTED_TEXT);
setBorder(StudioTheme.emptyBorder(index == 0 ? 2 : 10, 8, 0, 6));
setIcon(null);
setToolTipText(null);
shortcut = null;
hovered = false;
setOpaque(true);
return this;
}
setFont(StudioTheme.font());
setBorder(StudioTheme.emptyBorder(0, 8, 0, 8));
setIcon(MapViewIcon.of(row.view()));
setIconTextGap(StudioTheme.scale(ICON_TEXT_GAP));
setToolTipText(row.view().description());
shortcut = row.view().shortcutText();
hovered = !selected && list instanceof MapViewList viewList && viewList.hoveredRow == index;
setOpaque(!hovered);
return this;
}
@Override
protected void paintComponent(Graphics graphics) {
Graphics2D canvas = (Graphics2D) graphics.create();
try {
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
if (hovered) {
paintHoverBackground(canvas);
}
super.paintComponent(graphics);
if (shortcut != null) {
paintKeycap(canvas);
}
} finally {
canvas.dispose();
}
}
// Shaped like the selection the look and feel paints, so moving between them doesn't jump.
private void paintHoverBackground(Graphics2D canvas) {
Insets inset = UIManager.getInsets("List.selectionInsets");
int left = inset == null ? 0 : StudioTheme.scale(inset.left);
int right = inset == null ? 0 : StudioTheme.scale(inset.right);
int arc = StudioTheme.scale(UIManager.getInt("List.selectionArc"));
canvas.setColor(StudioTheme.HOVER_BACKGROUND);
canvas.fillRoundRect(left, 0, getWidth() - left - right, getHeight(), arc, arc);
}
private void paintKeycap(Graphics2D canvas) {
canvas.setFont(StudioTheme.smallFont());
int textWidth = canvas.getFontMetrics().stringWidth(shortcut);
int height = StudioTheme.scale(KEYCAP_SIZE);
int width = Math.max(height, textWidth + StudioTheme.scale(8));
int x = getWidth() - getInsets().right - width;
int y = (getHeight() - height) / 2;
int arc = StudioTheme.scale(KEYCAP_ARC);
canvas.setColor(StudioTheme.CONTROL_BORDER);
canvas.drawRoundRect(x, y, width - 1, height - 1, arc, arc);
canvas.setColor(StudioTheme.SECONDARY_TEXT);
int baseline = y + (height + canvas.getFontMetrics().getAscent() - canvas.getFontMetrics().getDescent()) / 2;
canvas.drawString(shortcut, x + (width - textWidth) / 2, baseline);
}
}
}
@@ -0,0 +1,497 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.core.gui.components.MapStyle;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.JSplitPane;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.SwingConstants;
import javax.swing.text.JTextComponent;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.KeyboardFocusManager;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.function.IntConsumer;
/**
* The studio map: one window showing the world as tiles in several views, with the palette, object and noise explorers
* as its Palettes, Objects and Noise views.
*/
public final class MapWindow implements MapCanvas.Listener {
private static final String MAP_CARD = "map";
private static final String PALETTES_CARD = "palettes";
private static final String OBJECTS_CARD = "objects";
private static final String NOISE_CARD = "noise";
private static final int SIDEBAR_WIDTH = 270;
private static final int SIDEBAR_MINIMUM_WIDTH = 200;
private static final int CONTENT_MINIMUM_WIDTH = 320;
private static final double PALETTE_RECENTER_DISTANCE_BLOCKS = 1D;
private static final int VIEW_LIST_SCROLL_INSETS = 4;
private static final int EXPLORER_DIVIDER_HEIGHT = 7;
private final JFrame frame;
private final PaletteExplorerStart start;
private final MapCanvas canvas;
private final MapViewList viewList;
private final JLabel viewTitle = new JLabel();
private final JPanel content = new JPanel(new CardLayout());
private final JPanel statusBars = new JPanel(new CardLayout());
private final JPanel sidebar = new JPanel(new BorderLayout());
private final IslandPanel sidebarIsland = new IslandPanel(sidebar);
private final JPanel inspectorSlot = new JPanel(new BorderLayout());
private final JComponent viewListScroll;
private final JSplitPane explorerSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
private final List<JComponent> mapControls = new ArrayList<>();
private final List<JComponent> explorerControls = new ArrayList<>();
private final List<JComponent> noiseControls = new ArrayList<>();
private final JToolBar toolbar = StudioToolbar.create();
private final JComponent resetViewButton = StudioToolbar.button("Reset view", "Go back to the starting position and zoom (R)",
this::resetExplorerView);
private final JToggleButton gridToggle = StudioToolbar.toggle("Grid", "Show chunk and region borders (G)");
private final JToggleButton entitiesToggle = StudioToolbar.toggle("Entities", "Show mobs and other entities (E)");
private final JToggleButton followToggle = StudioToolbar.toggle("Follow", "Keep your player in the middle (F)");
private final JToggleButton shadingToggle = StudioToolbar.toggle("Shading", "Shade hills and valleys on every view (S)");
private final JToggleButton bordersToggle = StudioToolbar.toggle("Borders", "Outline the regions on every view (B)");
private final MapLegendPanel legendPanel;
private NoiseExplorerGUI paletteExplorer;
private ObjectExplorer objectExplorer;
private NoiseExplorerGUI noiseExplorer;
private JComponent noiseSidebar;
private MapView currentView;
private double paletteCenterX;
private double paletteCenterZ;
private MapWindow(PaletteExplorerStart start) {
this.start = start;
this.frame = GuiHost.createFrame("Iris Map");
this.canvas = new MapCanvas(start.engine(), start.worldId(), start.playerId(), this);
this.viewList = new MapViewList(this::showView);
this.viewListScroll = StudioToolbar.scroll(viewList);
this.legendPanel = new MapLegendPanel(share -> canvas.toggleHighlight(share.label()));
canvas.centerOn(start.centerX(), start.centerZ());
buildFrame();
}
public static void openMap(PaletteExplorerStart start) {
EventQueue.invokeLater(() -> open(start, MapView.BIOMES));
}
public static void openPalettes(PaletteExplorerStart start) {
EventQueue.invokeLater(() -> open(start, MapView.PALETTES));
}
public static void openNoise(PaletteExplorerStart start) {
EventQueue.invokeLater(() -> open(start, MapView.NOISE));
}
private static void open(PaletteExplorerStart start, MapView initialView) {
StudioTheme.install();
MapWindow window = new MapWindow(start);
window.frame.setVisible(true);
window.canvas.start();
window.showView(initialView);
}
private void buildFrame() {
styleExplorerSplit();
content.add(canvas, MAP_CARD);
statusBars.add(canvas.statusBar(), MAP_CARD);
sidebar.add(viewListScroll, BorderLayout.CENTER);
frame.setLayout(new BorderLayout());
frame.add(buildToolbar(), BorderLayout.NORTH);
frame.add(new IslandOverlay(sidebarIsland, content, StudioTheme.scale(SIDEBAR_WIDTH),
StudioTheme.scale(SIDEBAR_MINIMUM_WIDTH), StudioTheme.scale(CONTENT_MINIMUM_WIDTH)), BorderLayout.CENTER);
frame.add(inspectorSlot, BorderLayout.EAST);
frame.add(statusBars, BorderLayout.SOUTH);
installShortcuts(frame.getRootPane());
keepClearOfIsland(canvas::setCoveredLeftWidth);
frame.setSize(GuiHost.fitToScreen(StudioTheme.scaledSize(1440, 880)));
frame.setMinimumSize(StudioTheme.scaledSize(820, 520));
frame.setLocationRelativeTo(null);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent event) {
close();
}
});
}
private JToolBar buildToolbar() {
viewTitle.setFont(StudioTheme.titleFont());
viewTitle.setBorder(StudioTheme.emptyBorder(0, 6, 0, 14));
toolbar.add(viewTitle);
mapControls.add(StudioToolbar.button("Refresh", "Redraw the map with the latest pack files (R)", canvas::refreshContent));
mapControls.add(StudioToolbar.button("Reset zoom", "Go back to the default zoom (\\)", canvas::resetZoom));
mapControls.add(gridToggle);
mapControls.add(entitiesToggle);
mapControls.add(followToggle);
mapControls.add(toolbarSeparator());
mapControls.add(shadingToggle);
mapControls.add(bordersToggle);
gridToggle.addActionListener(event -> canvas.toggleGrid());
entitiesToggle.addActionListener(event -> canvas.toggleEntities());
followToggle.addActionListener(event -> canvas.toggleFollow());
shadingToggle.addActionListener(event -> canvas.toggleShading());
bordersToggle.addActionListener(event -> canvas.toggleRegionBorders());
mapControls.forEach(toolbar::add);
explorerControls.add(resetViewButton);
explorerControls.forEach(toolbar::add);
toolbar.add(Box.createHorizontalGlue());
toolbar.add(StudioToolbar.themeButton());
JComponent help = StudioToolbar.button("Shortcuts", "Show every mouse and key shortcut (/)", canvas::toggleHelp);
mapControls.add(help);
toolbar.add(help);
return toolbar;
}
private static JComponent toolbarSeparator() {
JToolBar.Separator separator = new JToolBar.Separator();
separator.setOrientation(SwingConstants.VERTICAL);
return separator;
}
private void showView(MapView view) {
if (view == null || view == currentView) {
return;
}
MapView previousView = currentView;
currentView = view;
viewList.select(view);
viewTitle.setText(view.label());
viewTitle.setToolTipText(view.description());
if (view.isTileMap()) {
showMap(previousView, view);
} else if (view == MapView.PALETTES) {
showPalettes(previousView);
} else if (view == MapView.OBJECTS) {
showObjects();
} else {
showNoise();
}
frame.getRootPane().revalidate();
frame.getRootPane().repaint();
}
private void showMap(MapView previousView, MapView view) {
if (previousView == MapView.PALETTES && paletteExplorer != null) {
canvas.centerOn(paletteExplorer.centerX(), paletteExplorer.centerZ());
}
canvas.showView(view);
canvas.setActive(true);
showControls(mapControls);
useSidebar(viewListScroll);
inspectorSlot.removeAll();
inspectorSlot.add(legendPanel, BorderLayout.CENTER);
showCard(MAP_CARD);
canvas.requestFocusInWindow();
}
private void showPalettes(MapView previousView) {
boolean firstOpen = paletteExplorer == null;
if (firstOpen) {
createPaletteExplorer(previousView == null);
} else {
followMapIntoPalettes();
}
canvas.setActive(false);
showControls(explorerControls);
useSidebar(viewListAbove(paletteExplorer.paletteSidebar()));
inspectorSlot.removeAll();
inspectorSlot.add(paletteExplorer.paletteInspector(), BorderLayout.CENTER);
showCard(PALETTES_CARD);
EventQueue.invokeLater(() -> viewList.select(currentView));
if (firstOpen) {
paletteExplorer.openViewer();
}
}
private void showObjects() {
boolean firstOpen = objectExplorer == null;
if (firstOpen) {
createObjectExplorer();
}
canvas.setActive(false);
showControls(explorerControls);
useSidebar(viewListAbove(objectExplorer.browser()));
inspectorSlot.removeAll();
inspectorSlot.add(objectExplorer.overview(), BorderLayout.CENTER);
showCard(OBJECTS_CARD);
EventQueue.invokeLater(() -> viewList.select(currentView));
if (firstOpen) {
objectExplorer.open();
}
}
private void keepClearOfIsland(IntConsumer setCoveredLeftWidth) {
setCoveredLeftWidth.accept(sidebarIsland.getWidth());
sidebarIsland.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent event) {
setCoveredLeftWidth.accept(sidebarIsland.getWidth());
}
});
}
private void createObjectExplorer() {
objectExplorer = new ObjectExplorer(start.engine(), start.worldId());
keepClearOfIsland(objectExplorer.viewport()::setCoveredLeftWidth);
content.add(objectExplorer.view(), OBJECTS_CARD);
statusBars.add(objectExplorer.statusBar(), OBJECTS_CARD);
}
private void showNoise() {
boolean firstOpen = noiseExplorer == null;
if (firstOpen) {
createNoiseExplorer();
}
canvas.setActive(false);
showControls(noiseControls, explorerControls);
useSidebar(viewListAbove(noiseSidebar));
inspectorSlot.removeAll();
showCard(NOISE_CARD);
EventQueue.invokeLater(() -> viewList.select(currentView));
if (firstOpen) {
noiseExplorer.openViewer();
}
}
// Both explorers share one split, so the divider stays where it was dragged when switching between them.
private JSplitPane viewListAbove(JComponent explorerSidebar) {
boolean firstUse = explorerSplit.getBottomComponent() == null;
explorerSplit.setTopComponent(viewListScroll);
explorerSplit.setBottomComponent(explorerSidebar);
if (firstUse) {
EventQueue.invokeLater(this::fitViewListAboveExplorer);
}
return explorerSplit;
}
private void styleExplorerSplit() {
explorerSplit.setBorder(BorderFactory.createEmptyBorder());
explorerSplit.setResizeWeight(0D);
explorerSplit.setBackground(StudioTheme.PANEL_BACKGROUND);
explorerSplit.setDividerSize(StudioTheme.scale(EXPLORER_DIVIDER_HEIGHT));
explorerSplit.putClientProperty("FlatLaf.style", "style: grip");
}
// Every view stays in sight, so the divider starts below the last one and can only be dragged further down. Measured
// once the sidebar is laid out, since the list's rows are taller before they know their width.
private void fitViewListAboveExplorer() {
int fullHeight = viewList.getPreferredSize().height + VIEW_LIST_SCROLL_INSETS;
viewListScroll.setMinimumSize(new Dimension(0, fullHeight));
explorerSplit.setDividerLocation(fullHeight);
}
private void createNoiseExplorer() {
noiseExplorer = NoiseExplorerGUI.createNoiseExplorer(start.engine());
noiseSidebar = noiseExplorer.noiseSidebar();
noiseControls.addAll(noiseExplorer.noiseControls());
noiseControls.forEach(control -> toolbar.add(control, toolbar.getComponentIndex(resetViewButton)));
content.add(noiseExplorer, NOISE_CARD);
statusBars.add(noiseExplorer.statusBar(), NOISE_CARD);
}
private void createPaletteExplorer(boolean openedOnPalettes) {
PaletteExplorerStart explorerStart = openedOnPalettes
? start
: new PaletteExplorerStart(start.engine(), start.worldId(), start.playerId(), null, canvas.centerX(), canvas.centerZ());
paletteExplorer = NoiseExplorerGUI.createPaletteExplorer(explorerStart);
content.add(paletteExplorer, PALETTES_CARD);
statusBars.add(paletteExplorer.statusBar(), PALETTES_CARD);
rememberPaletteCenter(explorerStart.centerX(), explorerStart.centerZ());
if (!openedOnPalettes) {
showSurfaceBiomePalettes(explorerStart.centerX(), explorerStart.centerZ());
}
}
// Coming back from the map only moves the palettes when the map was moved, so a chosen layer survives a quick look.
private void followMapIntoPalettes() {
double movedX = canvas.centerX() - paletteCenterX;
double movedZ = canvas.centerZ() - paletteCenterZ;
if (Math.hypot(movedX, movedZ) < PALETTE_RECENTER_DISTANCE_BLOCKS) {
return;
}
paletteExplorer.centerOn(canvas.centerX(), canvas.centerZ());
rememberPaletteCenter(canvas.centerX(), canvas.centerZ());
showSurfaceBiomePalettes(canvas.centerX(), canvas.centerZ());
}
@Override
public void onShowPalettesAt(double worldX, double worldZ) {
canvas.centerOn(worldX, worldZ);
showView(MapView.PALETTES);
}
private void showSurfaceBiomePalettes(double worldX, double worldZ) {
canvas.findSurfaceBiome(worldX, worldZ, biomeKey -> {
if (paletteExplorer != null) {
paletteExplorer.showPalettesAt(worldX, worldZ, biomeKey);
}
});
}
private void rememberPaletteCenter(double worldX, double worldZ) {
paletteCenterX = worldX;
paletteCenterZ = worldZ;
}
@Override
public void onTogglesChanged() {
gridToggle.setSelected(canvas.isGridVisible());
entitiesToggle.setSelected(canvas.areEntitiesVisible());
followToggle.setSelected(canvas.isFollowing());
shadingToggle.setSelected(canvas.style().shading());
bordersToggle.setSelected(canvas.style().regionBorders());
}
@Override
public void onLegendChanged(String title, List<MapCensus.Share> shares, MapStyle.Highlight highlight) {
legendPanel.show(title, shares, highlight);
}
private void resetExplorerView() {
if (currentView == MapView.OBJECTS) {
objectExplorer.resetCamera();
return;
}
NoiseExplorerGUI explorer = currentView == MapView.NOISE ? noiseExplorer : paletteExplorer;
if (explorer != null) {
explorer.resetViewport();
}
}
private void useSidebar(JComponent component) {
sidebar.removeAll();
sidebar.add(component, BorderLayout.CENTER);
}
private void showCard(String card) {
((CardLayout) content.getLayout()).show(content, card);
((CardLayout) statusBars.getLayout()).show(statusBars, card);
}
@SafeVarargs
private void showControls(List<JComponent>... visibleControls) {
for (List<JComponent> controls : List.of(mapControls, explorerControls, noiseControls)) {
controls.forEach(control -> control.setVisible(false));
}
for (List<JComponent> controls : visibleControls) {
controls.forEach(control -> control.setVisible(true));
}
}
private void installShortcuts(JRootPane rootPane) {
for (MapView view : MapView.values()) {
bind(rootPane, KeyStroke.getKeyStroke(view.shortcutKey(), 0), "view-" + view.name(), () -> showView(view));
}
bind(rootPane, KeyStroke.getKeyStroke(KeyEvent.VK_M, 0), "next-view", this::showNextView);
bind(rootPane, KeyStroke.getKeyStroke(KeyEvent.VK_R, 0), "refresh", this::refreshOrReset);
bindMap(rootPane, KeyEvent.VK_F, 0, "follow", canvas::toggleFollow);
bindMap(rootPane, KeyEvent.VK_G, 0, "grid", canvas::toggleGrid);
bindMap(rootPane, KeyEvent.VK_E, 0, "entities", canvas::toggleEntities);
bindMap(rootPane, KeyEvent.VK_F3, 0, "debug", canvas::toggleDebug);
bindMap(rootPane, KeyEvent.VK_H, 0, "highlight", canvas::highlightUnderCursor);
bindMap(rootPane, KeyEvent.VK_ESCAPE, 0, "dismiss", canvas::dismissEmphasis);
bindMap(rootPane, KeyEvent.VK_S, 0, "shading", canvas::toggleShading);
bindMap(rootPane, KeyEvent.VK_B, 0, "borders", canvas::toggleRegionBorders);
bindMap(rootPane, KeyEvent.VK_EQUALS, 0, "zoom-in", canvas::zoomIn);
bindMap(rootPane, KeyEvent.VK_EQUALS, KeyEvent.SHIFT_DOWN_MASK, "zoom-in-plus", canvas::zoomIn);
bindMap(rootPane, KeyEvent.VK_ADD, 0, "zoom-in-keypad", canvas::zoomIn);
bindMap(rootPane, KeyEvent.VK_MINUS, 0, "zoom-out", canvas::zoomOut);
bindMap(rootPane, KeyEvent.VK_SUBTRACT, 0, "zoom-out-keypad", canvas::zoomOut);
bindMap(rootPane, KeyEvent.VK_BACK_SLASH, 0, "reset-zoom", canvas::resetZoom);
bindMap(rootPane, KeyEvent.VK_SLASH, 0, "help", canvas::toggleHelp);
bindMap(rootPane, KeyEvent.VK_SLASH, KeyEvent.SHIFT_DOWN_MASK, "help-question", canvas::toggleHelp);
bind(rootPane, KeyStroke.getKeyStroke(KeyEvent.VK_SHIFT, KeyEvent.SHIFT_DOWN_MASK, false), "details-on",
() -> canvas.setDetailedHover(true));
bind(rootPane, KeyStroke.getKeyStroke(KeyEvent.VK_SHIFT, 0, true), "details-off", () -> canvas.setDetailedHover(false));
}
private void bindMap(JRootPane rootPane, int keyCode, int modifiers, String name, Runnable action) {
bind(rootPane, KeyStroke.getKeyStroke(keyCode, modifiers), name, () -> {
if (currentView != null && currentView.isTileMap()) {
action.run();
}
});
}
// Keys typed into a search box belong to that box, not to the window's shortcuts.
private static void bind(JRootPane rootPane, KeyStroke keyStroke, String name, Runnable action) {
rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(keyStroke, name);
rootPane.getActionMap().put(name, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent event) {
if (!(KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() instanceof JTextComponent)) {
action.run();
}
}
});
}
private void showNextView() {
MapView[] views = MapView.values();
showView(views[(currentView.ordinal() + 1) % views.length]);
}
private void refreshOrReset() {
if (currentView.isTileMap()) {
canvas.refreshContent();
} else {
resetExplorerView();
}
}
private void close() {
canvas.close();
if (paletteExplorer != null) {
paletteExplorer.close();
}
if (objectExplorer != null) {
objectExplorer.close();
}
if (noiseExplorer != null) {
noiseExplorer.close();
}
}
}
@@ -0,0 +1,155 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.volmit.iris.engine.object.IrisBiomeCustom;
import org.bukkit.NamespacedKey;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.ZipFile;
// Works out biome tint colours the way Minecraft does, from the colour maps and vanilla biome files in the client jar.
final class MinecraftBiomeColors {
private static final String COLOR_MAP_FOLDER = "assets/minecraft/textures/colormap/";
private static final int COLOR_MAP_SIZE = 256;
private static final int GRASS_FALLBACK = 0xFF00FF;
private static final int FOLIAGE_FALLBACK = 0x48B518;
private static final int DRY_FOLIAGE_FALLBACK = 0x5C3C32;
private static final float UNKNOWN_BIOME_TEMPERATURE = 0.5F;
private static final float UNKNOWN_BIOME_DOWNFALL = 0.5F;
private static final int UNKNOWN_BIOME_WATER = 0x3F76E4;
private static final int UNPARSEABLE_COLOR = 0x000000;
private final ZipFile clientJar;
private final ColorMap grassMap;
private final ColorMap foliageMap;
private final ColorMap dryFoliageMap;
private final Map<NamespacedKey, BiomeTintColors> vanillaBiomes = new ConcurrentHashMap<>();
private MinecraftBiomeColors(ZipFile clientJar, ColorMap grassMap, ColorMap foliageMap, ColorMap dryFoliageMap) {
this.clientJar = clientJar;
this.grassMap = grassMap;
this.foliageMap = foliageMap;
this.dryFoliageMap = dryFoliageMap;
}
static MinecraftBiomeColors load(ZipFile clientJar) throws IOException {
return new MinecraftBiomeColors(
clientJar,
ColorMap.read(clientJar, "grass.png", GRASS_FALLBACK),
ColorMap.read(clientJar, "foliage.png", FOLIAGE_FALLBACK),
ColorMap.read(clientJar, "dry_foliage.png", DRY_FOLIAGE_FALLBACK)
);
}
BiomeTintColors vanilla(NamespacedKey biome) {
return vanillaBiomes.computeIfAbsent(biome, this::readVanillaBiome);
}
// Iris registers custom biomes as data pack biomes built from these fields, see IrisBiomeCustom#generateJson.
BiomeTintColors custom(IrisBiomeCustom biome) {
float temperature = (float) biome.getTemperature();
float downfall = (float) biome.getHumidity();
return new BiomeTintColors(
biome.getGrassColor().isEmpty() ? grassMap.colorAt(temperature, downfall) : parseIrisColor(biome.getGrassColor()),
BiomeTintColors.GrassColorModifier.NONE,
biome.getFoliageColor().isEmpty() ? foliageMap.colorAt(temperature, downfall) : parseIrisColor(biome.getFoliageColor()),
dryFoliageMap.colorAt(temperature, downfall),
parseIrisColor(biome.getWaterColor())
);
}
private BiomeTintColors readVanillaBiome(NamespacedKey biome) {
JsonObject definition = readBiomeDefinition(biome);
if (definition == null) {
return fromClimate(UNKNOWN_BIOME_TEMPERATURE, UNKNOWN_BIOME_DOWNFALL, new JsonObject());
}
JsonObject effects = definition.has("effects") ? definition.getAsJsonObject("effects") : new JsonObject();
return fromClimate(definition.get("temperature").getAsFloat(), definition.get("downfall").getAsFloat(), effects);
}
private BiomeTintColors fromClimate(float temperature, float downfall, JsonObject effects) {
return new BiomeTintColors(
effects.has("grass_color") ? parseVanillaColor(effects.get("grass_color")) : grassMap.colorAt(temperature, downfall),
effects.has("grass_color_modifier")
? BiomeTintColors.GrassColorModifier.named(effects.get("grass_color_modifier").getAsString())
: BiomeTintColors.GrassColorModifier.NONE,
effects.has("foliage_color") ? parseVanillaColor(effects.get("foliage_color")) : foliageMap.colorAt(temperature, downfall),
effects.has("dry_foliage_color") ? parseVanillaColor(effects.get("dry_foliage_color")) : dryFoliageMap.colorAt(temperature, downfall),
effects.has("water_color") ? parseVanillaColor(effects.get("water_color")) : UNKNOWN_BIOME_WATER
);
}
private JsonObject readBiomeDefinition(NamespacedKey biome) {
try {
return ClientJarFiles.readJson(clientJar, "data/" + biome.getNamespace() + "/worldgen/biome/" + biome.getKey() + ".json");
} catch (IOException | RuntimeException error) {
return null;
}
}
// Older versions store colours as plain numbers, newer ones as "#rrggbb" strings.
private static int parseVanillaColor(JsonElement color) {
if (color.getAsJsonPrimitive().isNumber()) {
return color.getAsInt() & 0xFFFFFF;
}
return Integer.parseInt(color.getAsString().substring(1), 16);
}
private static int parseIrisColor(String color) {
try {
return Color.decode((color.startsWith("#") ? color : "#" + color).trim()).getRGB() & 0xFFFFFF;
} catch (RuntimeException error) {
return UNPARSEABLE_COLOR;
}
}
// Minecraft's ColorMapColorUtil: temperature picks the column and temperature times downfall picks the row.
private record ColorMap(int[] pixels, int fallback) {
private static ColorMap read(ZipFile clientJar, String name, int fallback) throws IOException {
BufferedImage image = ClientJarFiles.readImage(clientJar, COLOR_MAP_FOLDER + name);
if (image == null) {
throw new IOException("Minecraft colour map " + name + " is missing");
}
int[] pixels = new int[COLOR_MAP_SIZE * COLOR_MAP_SIZE];
for (int y = 0; y < COLOR_MAP_SIZE; y++) {
for (int x = 0; x < COLOR_MAP_SIZE; x++) {
pixels[y * COLOR_MAP_SIZE + x] = image.getRGB(x, y) & 0xFFFFFF;
}
}
return new ColorMap(pixels, fallback);
}
private int colorAt(float temperature, float downfall) {
double clampedTemperature = Math.clamp(temperature, 0F, 1F);
double clampedDownfall = Math.clamp(downfall, 0F, 1F) * clampedTemperature;
int x = (int) ((1D - clampedTemperature) * 255D);
int y = (int) ((1D - clampedDownfall) * 255D);
int index = y << 8 | x;
return index >= pixels.length ? fallback : pixels[index];
}
}
}
@@ -0,0 +1,85 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
// Mirrors Minecraft's BlockColors: which colour each tinted face of a block is multiplied with in the world.
final class MinecraftBlockTints {
static final int NO_TINT = 0xFFFFFF;
private static final Set<String> GRASS_TINTED = Set.of(
"grass_block", "short_grass", "fern", "potted_fern", "bush", "tall_grass", "large_fern", "sugar_cane");
private static final Set<String> GRASS_TINTED_ON_SECOND_LAYER = Set.of("pink_petals", "wildflowers");
private static final Set<String> FOLIAGE_TINTED = Set.of(
"oak_leaves", "jungle_leaves", "acacia_leaves", "dark_oak_leaves", "vine", "mangrove_leaves");
private static final Set<String> WATER_TINTED_BY_FLUID_RENDERER = Set.of("water", "bubble_column");
private static final Set<String> GROWING_STEMS = Set.of("melon_stem", "pumpkin_stem");
private static final Map<String, Integer> FIXED_TINTS = Map.of(
"spruce_leaves", 0x619961,
"birch_leaves", 0x80A755,
"attached_melon_stem", 0xE0C71C,
"attached_pumpkin_stem", 0xE0C71C,
"lily_pad", 0x208030
);
private MinecraftBlockTints() {
}
static Optional<BiomeTint> biomeTint(String blockName, int tintIndex) {
if (WATER_TINTED_BY_FLUID_RENDERER.contains(blockName)) {
return Optional.of(BiomeTint.WATER);
}
if (GRASS_TINTED_ON_SECOND_LAYER.contains(blockName)) {
return tintIndex == 1 ? Optional.of(BiomeTint.GRASS) : Optional.empty();
}
if (tintIndex != 0) {
return Optional.empty();
}
if (GRASS_TINTED.contains(blockName)) {
return Optional.of(BiomeTint.GRASS);
}
if (FOLIAGE_TINTED.contains(blockName)) {
return Optional.of(BiomeTint.FOLIAGE);
}
if (blockName.equals("leaf_litter")) {
return Optional.of(BiomeTint.DRY_FOLIAGE);
}
if (blockName.equals("water_cauldron")) {
return Optional.of(BiomeTint.WATER);
}
return Optional.empty();
}
static int fixedTint(String blockName, Map<String, String> properties, int tintIndex) {
if (tintIndex != 0) {
return NO_TINT;
}
if (GROWING_STEMS.contains(blockName)) {
return stemTint(Integer.parseInt(properties.getOrDefault("age", "0")));
}
return FIXED_TINTS.getOrDefault(blockName, NO_TINT);
}
private static int stemTint(int age) {
return (age * 32) << 16 | (255 - age * 8) << 8 | (age * 4);
}
}
@@ -0,0 +1,145 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.volmit.iris.Iris;
import org.bukkit.Bukkit;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.HexFormat;
import java.util.concurrent.CompletableFuture;
import java.util.zip.ZipFile;
final class MinecraftClientJar {
private static final URI VERSION_MANIFEST = URI.create("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json");
private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(15);
private static final Duration DOWNLOAD_TIMEOUT = Duration.ofMinutes(10);
private static final HttpClient HTTP = HttpClient.newBuilder()
.connectTimeout(CONNECT_TIMEOUT)
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
private MinecraftClientJar() {
}
static boolean isCached() {
return cachedJar().exists();
}
static CompletableFuture<ZipFile> open() {
return CompletableFuture.supplyAsync(() -> {
try {
File jar = cachedJar();
if (!jar.exists()) {
download(Bukkit.getMinecraftVersion(), jar.toPath());
}
return new ZipFile(jar);
} catch (IOException | InterruptedException error) {
throw new IllegalStateException(readableReason(error), error);
}
}, MinecraftClientJar::runInBackground);
}
private static void runInBackground(Runnable task) {
Thread thread = new Thread(task, "Iris Minecraft Client Download");
thread.setDaemon(true);
thread.start();
}
private static File cachedJar() {
return Iris.instance.getDataFile("cache", "minecraft-client", Bukkit.getMinecraftVersion() + ".jar");
}
private static void download(String version, Path destination) throws IOException, InterruptedException {
JsonObject versionInfo = fetchJson(findVersionUrl(version));
JsonObject client = versionInfo.getAsJsonObject("downloads").getAsJsonObject("client");
URI clientUrl = URI.create(client.get("url").getAsString());
String expectedSha1 = client.get("sha1").getAsString();
Path partial = destination.resolveSibling(destination.getFileName() + ".part");
HttpResponse<InputStream> response = HTTP.send(
HttpRequest.newBuilder(clientUrl).timeout(DOWNLOAD_TIMEOUT).build(),
HttpResponse.BodyHandlers.ofInputStream()
);
requireSuccess(response.statusCode(), clientUrl);
String actualSha1;
try (DigestInputStream body = new DigestInputStream(response.body(), sha1())) {
Files.copy(body, partial, StandardCopyOption.REPLACE_EXISTING);
actualSha1 = HexFormat.of().formatHex(body.getMessageDigest().digest());
}
if (!actualSha1.equalsIgnoreCase(expectedSha1)) {
Files.deleteIfExists(partial);
throw new IOException("the downloaded client jar was damaged (checksum mismatch)");
}
Files.move(partial, destination, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
}
private static URI findVersionUrl(String version) throws IOException, InterruptedException {
for (var entry : fetchJson(VERSION_MANIFEST).getAsJsonArray("versions")) {
JsonObject candidate = entry.getAsJsonObject();
if (candidate.get("id").getAsString().equals(version)) {
return URI.create(candidate.get("url").getAsString());
}
}
throw new IOException("Mojang doesn't list Minecraft " + version);
}
private static JsonObject fetchJson(URI url) throws IOException, InterruptedException {
HttpResponse<String> response = HTTP.send(
HttpRequest.newBuilder(url).timeout(CONNECT_TIMEOUT).build(),
HttpResponse.BodyHandlers.ofString()
);
requireSuccess(response.statusCode(), url);
return JsonParser.parseString(response.body()).getAsJsonObject();
}
private static void requireSuccess(int statusCode, URI url) throws IOException {
if (statusCode / 100 != 2) {
throw new IOException(url.getHost() + " answered HTTP " + statusCode);
}
}
private static MessageDigest sha1() {
try {
return MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException error) {
throw new IllegalStateException(error);
}
}
private static String readableReason(Throwable error) {
String message = error.getMessage();
return message == null || message.isBlank() ? error.getClass().getSimpleName() : message;
}
}
@@ -0,0 +1,43 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import java.awt.Color;
interface NoiseColorScale {
int colorFinite(double value);
default int colorAt(double value, double worldX, double worldZ) {
return colorFinite(value);
}
int colorInvalid();
default int colorInvalidAt(double worldX, double worldZ) {
return colorInvalid();
}
Color displayColorNormalized(double normalized);
String label();
double minimum();
double maximum();
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,124 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import java.awt.Color;
enum NoisePalette implements NoiseColorScale {
TERRAIN("Terrain", 0D, 1D, new int[]{0x071A2F, 0x155E75, 0x2A9D8F, 0xE9C46A, 0xF4F1DE}),
SIGNED("Signed", -1D, 1D, new int[]{0x173B66, 0x4F86C6, 0xE7EDF3, 0xE89555, 0x9D3B35}),
GRAYSCALE("Grayscale", 0D, 1D, new int[]{0x050505, 0xFFFFFF});
static final int INVALID_COLOR = 0xFF2DAA;
private final String label;
private final double minimum;
private final double maximum;
private final int[] lookup;
private final Color[] displayColors;
NoisePalette(String label, double minimum, double maximum, int[] stops) {
this.label = label;
this.minimum = minimum;
this.maximum = maximum;
this.lookup = buildLookup(stops);
this.displayColors = buildDisplayColors(lookup);
}
int color(double value) {
if (!Double.isFinite(value)) {
return INVALID_COLOR;
}
return colorFinite(value);
}
@Override
public int colorFinite(double value) {
double normalized = (value - minimum) / (maximum - minimum);
return colorNormalized(normalized);
}
int colorNormalized(double normalized) {
double clipped = Math.max(0D, Math.min(1D, normalized));
return lookup[(int) Math.round(clipped * (lookup.length - 1))];
}
@Override
public int colorInvalid() {
return INVALID_COLOR;
}
@Override
public Color displayColorNormalized(double normalized) {
double clipped = Math.max(0D, Math.min(1D, normalized));
return displayColors[(int) Math.round(clipped * (displayColors.length - 1))];
}
@Override
public String label() {
return label;
}
@Override
public double minimum() {
return minimum;
}
@Override
public double maximum() {
return maximum;
}
@Override
public String toString() {
return label;
}
private static int[] buildLookup(int[] stops) {
int[] values = new int[256];
for (int index = 0; index < values.length; index++) {
double position = (index / 255D) * (stops.length - 1);
int lowerIndex = Math.min(stops.length - 2, (int) position);
double fraction = position - lowerIndex;
values[index] = interpolate(stops[lowerIndex], stops[lowerIndex + 1], fraction);
}
return values;
}
private static Color[] buildDisplayColors(int[] lookup) {
Color[] colors = new Color[lookup.length];
for (int index = 0; index < lookup.length; index++) {
colors[index] = new Color(lookup[index]);
}
return colors;
}
private static int interpolate(int from, int to, double fraction) {
int red = channel(from, 16, to, fraction);
int green = channel(from, 8, to, fraction);
int blue = channel(from, 0, to, fraction);
return (red << 16) | (green << 8) | blue;
}
private static int channel(int from, int shift, int to, double fraction) {
int fromChannel = (from >>> shift) & 0xFF;
int toChannel = (to >>> shift) & 0xFF;
return (int) Math.round(fromChannel + ((toChannel - fromChannel) * fraction));
}
}
@@ -0,0 +1,378 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.util.function.NoiseProvider;
import java.awt.image.BufferedImage;
import java.util.Objects;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
final class NoiseRenderCoordinator implements AutoCloseable {
private static final int BANDS_PER_WORKER = 4;
private final int workerCount;
private final Listener listener;
private final ThreadPoolExecutor workers;
private final AtomicReference<RenderGeneration> latestRequested = new AtomicReference<>();
private final AtomicLong latestRevision = new AtomicLong();
private final AtomicBoolean closed = new AtomicBoolean();
NoiseRenderCoordinator(Listener listener) {
this(Math.min(4, Math.max(1, Runtime.getRuntime().availableProcessors() / 2)), listener);
}
NoiseRenderCoordinator(int workerCount, Listener listener) {
this.workerCount = Math.max(1, workerCount);
this.listener = Objects.requireNonNull(listener, "listener");
AtomicInteger threadIds = new AtomicInteger();
ThreadFactory workerThreadFactory = runnable -> {
Thread thread = new Thread(runnable, "Iris Noise Renderer " + threadIds.incrementAndGet());
thread.setDaemon(true);
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
};
workers = new ThreadPoolExecutor(this.workerCount, this.workerCount, 30L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), workerThreadFactory);
workers.allowCoreThreadTimeOut(true);
GuiHost.interruptOnPluginShutdown(workers);
}
void request(Request request) {
Objects.requireNonNull(request, "request");
if (closed.get()) {
return;
}
latestRevision.accumulateAndGet(request.revision(), Math::max);
RenderGeneration generation = new RenderGeneration(request, Math.min(workerCount * BANDS_PER_WORKER, outputHeight(request)));
latestRequested.set(generation);
listener.onRenderStarted(request);
for (int band = 0; band < generation.bandCount(); band++) {
int bandIndex = band;
try {
workers.execute(() -> renderBand(generation, bandIndex));
} catch (RejectedExecutionException rejected) {
generation.fail(rejected);
finishBand(generation);
}
}
}
void cancel(long revision) {
latestRevision.accumulateAndGet(revision, Math::max);
latestRequested.set(null);
}
boolean isClosed() {
return closed.get();
}
static int sampleStepForBudget(int width, int height, long sampleBudget) {
if (width < 1 || height < 1 || sampleBudget < 1L) {
throw new IllegalArgumentException("Invalid noise sample budget");
}
long totalSamples = (long) width * height;
int sampleStep = Math.max(1, (int) Math.ceil(Math.sqrt(totalSamples / (double) sampleBudget)));
while (sampleCount(width, height, sampleStep) > sampleBudget) {
sampleStep++;
}
return sampleStep;
}
static long sampleCount(int width, int height, int sampleStep) {
if (width < 1 || height < 1 || sampleStep < 1) {
throw new IllegalArgumentException("Invalid noise sample dimensions");
}
long outputWidth = ((long) width + sampleStep - 1L) / sampleStep;
long outputHeight = ((long) height + sampleStep - 1L) / sampleStep;
return outputWidth * outputHeight;
}
static int nextRefinementStep(int width, int height, int currentStep, long sampleBudget) {
if (currentStep <= 1) {
throw new IllegalArgumentException("Noise refinement requires a coarse input");
}
return Math.min(currentStep, sampleStepForBudget(width, height, sampleBudget));
}
static long timeBoundSampleBudget(long completedSamples, double milliseconds, double targetMilliseconds,
long minimumBudget, long maximumBudget) {
if (completedSamples < 1L
|| !Double.isFinite(milliseconds)
|| milliseconds <= 0D
|| !Double.isFinite(targetMilliseconds)
|| targetMilliseconds <= 0D
|| minimumBudget < 1L
|| maximumBudget < minimumBudget) {
throw new IllegalArgumentException("Invalid timed noise sample budget");
}
double projectedSamples = Math.ceil((completedSamples / milliseconds) * targetMilliseconds);
long timeBoundBudget = (long) Math.min(maximumBudget, projectedSamples);
return Math.max(minimumBudget, timeBoundBudget);
}
@Override
public void close() {
if (!closed.compareAndSet(false, true)) {
return;
}
latestRevision.incrementAndGet();
latestRequested.set(null);
workers.shutdownNow();
}
// Bands of a frame that was replaced are skipped, so a new frame only waits for the rows already being drawn.
private void renderBand(RenderGeneration generation, int bandIndex) {
try {
if (isCurrent(generation)) {
renderRows(generation, bandIndex);
}
} catch (Throwable error) {
generation.fail(error);
} finally {
finishBand(generation);
}
}
private void finishBand(RenderGeneration generation) {
if (!generation.completeBand() || !isCurrent(generation)) {
return;
}
Throwable failure = generation.failure();
if (failure != null) {
listener.onRenderFailed(generation.request(), failure);
return;
}
listener.onRenderCompleted(generation.result());
}
private void renderRows(RenderGeneration generation, int bandIndex) {
Request request = generation.request();
int outputWidth = generation.outputWidth();
int outputHeight = generation.outputHeight();
int[] pixels = generation.pixels();
BandStats stats = generation.bandStats(bandIndex);
int fromY = (outputHeight * bandIndex) / generation.bandCount();
int toY = (outputHeight * (bandIndex + 1)) / generation.bandCount();
double sampleOffset = request.sampleStep() * 0.5D;
double startWorldX = request.viewport().worldX(sampleOffset, request.width());
double worldStep = request.viewport().blocksPerPixel() * request.sampleStep();
NoiseColorScale colorScale = request.colorScale();
double paletteMinimum = colorScale.minimum();
double paletteMaximum = colorScale.maximum();
for (int y = fromY; y < toY; y++) {
double screenY = (y * (double) request.sampleStep()) + sampleOffset;
double worldZ = request.viewport().worldZ(screenY, request.height());
double worldX = startWorldX;
int pixelIndex = y * outputWidth;
boolean centerRow = y == outputHeight / 2;
for (int x = 0; x < outputWidth; x++) {
if ((x & 31) == 0 && (!isCurrent(generation) || Thread.currentThread().isInterrupted())) {
return;
}
double value = request.sampler().noise(worldX, worldZ);
boolean center = centerRow && x == outputWidth / 2;
if (Double.isFinite(value)) {
pixels[pixelIndex++] = colorScale.colorAt(value, worldX, worldZ);
stats.acceptFinite(value, center, paletteMinimum, paletteMaximum);
} else {
pixels[pixelIndex++] = colorScale.colorInvalidAt(worldX, worldZ);
stats.acceptInvalid(value, center);
}
worldX += worldStep;
}
}
}
private boolean isCurrent(RenderGeneration generation) {
return !closed.get()
&& latestRequested.get() == generation
&& generation.request().revision() >= latestRevision.get();
}
private static int outputWidth(Request request) {
return Math.max(1, (request.width() + request.sampleStep() - 1) / request.sampleStep());
}
private static int outputHeight(Request request) {
return Math.max(1, (request.height() + request.sampleStep() - 1) / request.sampleStep());
}
private static RenderStats combine(BandStats[] bands) {
long samples = 0L;
long underflow = 0L;
long overflow = 0L;
long invalid = 0L;
double minimum = Double.POSITIVE_INFINITY;
double maximum = Double.NEGATIVE_INFINITY;
double centerValue = Double.NaN;
for (BandStats band : bands) {
samples += band.samples;
underflow += band.underflow;
overflow += band.overflow;
invalid += band.invalid;
minimum = Math.min(minimum, band.minimum);
maximum = Math.max(maximum, band.maximum);
if (!Double.isNaN(band.centerValue)) {
centerValue = band.centerValue;
}
}
if (minimum == Double.POSITIVE_INFINITY) {
minimum = Double.NaN;
maximum = Double.NaN;
}
return new RenderStats(samples, minimum, maximum, centerValue, underflow, overflow, invalid);
}
interface Listener {
void onRenderStarted(Request request);
void onRenderCompleted(Result result);
void onRenderFailed(Request request, Throwable error);
}
record Request(long revision, NoiseProvider sampler, NoiseViewport viewport, NoiseColorScale colorScale,
int width, int height, int sampleStep) {
Request {
Objects.requireNonNull(sampler, "sampler");
Objects.requireNonNull(viewport, "viewport");
Objects.requireNonNull(colorScale, "colorScale");
if (revision < 0L || width < 1 || height < 1 || sampleStep < 1) {
throw new IllegalArgumentException("Invalid noise render request");
}
}
}
record Result(Request request, BufferedImage image, double milliseconds, long samples, double minimum,
double maximum, double centerValue, long underflow, long overflow, long invalid) {
}
private static final class RenderGeneration {
private final Request request;
private final long startedNanos = System.nanoTime();
private final int outputWidth;
private final int outputHeight;
private final int[] pixels;
private final BandStats[] bandStats;
private final AtomicInteger remainingBands;
private final AtomicReference<Throwable> failure = new AtomicReference<>();
private RenderGeneration(Request request, int bandCount) {
this.request = request;
this.outputWidth = NoiseRenderCoordinator.outputWidth(request);
this.outputHeight = NoiseRenderCoordinator.outputHeight(request);
this.pixels = new int[outputWidth * outputHeight];
this.bandStats = new BandStats[Math.max(1, bandCount)];
for (int band = 0; band < bandStats.length; band++) {
bandStats[band] = new BandStats();
}
this.remainingBands = new AtomicInteger(bandStats.length);
}
private Request request() {
return request;
}
private int bandCount() {
return bandStats.length;
}
private int outputWidth() {
return outputWidth;
}
private int outputHeight() {
return outputHeight;
}
private int[] pixels() {
return pixels;
}
private BandStats bandStats(int band) {
return bandStats[band];
}
private void fail(Throwable error) {
failure.compareAndSet(null, error);
}
private Throwable failure() {
return failure.get();
}
// True for the band that finishes last, which is the one that publishes the frame.
private boolean completeBand() {
return remainingBands.decrementAndGet() == 0;
}
private Result result() {
BufferedImage image = GuiHost.uncachedImage(pixels, outputWidth, outputHeight);
RenderStats combined = combine(bandStats);
double milliseconds = (System.nanoTime() - startedNanos) / 1_000_000D;
return new Result(request, image, milliseconds, combined.samples, combined.minimum, combined.maximum,
combined.centerValue, combined.underflow, combined.overflow, combined.invalid);
}
}
private static final class BandStats {
private long samples;
private long underflow;
private long overflow;
private long invalid;
private double minimum = Double.POSITIVE_INFINITY;
private double maximum = Double.NEGATIVE_INFINITY;
private double centerValue = Double.NaN;
private void acceptFinite(double value, boolean center, double paletteMinimum, double paletteMaximum) {
samples++;
if (center) {
centerValue = value;
}
minimum = Math.min(minimum, value);
maximum = Math.max(maximum, value);
if (value < paletteMinimum) {
underflow++;
} else if (value > paletteMaximum) {
overflow++;
}
}
private void acceptInvalid(double value, boolean center) {
samples++;
invalid++;
if (center) {
centerValue = value;
}
}
}
private record RenderStats(long samples, double minimum, double maximum, double centerValue,
long underflow, long overflow, long invalid) {
}
}
@@ -0,0 +1,70 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
record NoiseViewport(double centerX, double centerZ, double blocksPerPixel) {
static final double MIN_BLOCKS_PER_PIXEL = 0.0001D;
static final double MAX_BLOCKS_PER_PIXEL = 1_000_000D;
NoiseViewport {
if (!Double.isFinite(centerX) || !Double.isFinite(centerZ)) {
throw new IllegalArgumentException("Viewport center must be finite");
}
if (!Double.isFinite(blocksPerPixel) || blocksPerPixel <= 0D) {
throw new IllegalArgumentException("Viewport scale must be finite and positive");
}
}
double worldX(double screenX, int width) {
return centerX + ((screenX - (width / 2D)) * blocksPerPixel);
}
double worldZ(double screenZ, int height) {
return centerZ + ((screenZ - (height / 2D)) * blocksPerPixel);
}
double screenX(double worldX, int width) {
return ((worldX - centerX) / blocksPerPixel) + (width / 2D);
}
double screenZ(double worldZ, int height) {
return ((worldZ - centerZ) / blocksPerPixel) + (height / 2D);
}
NoiseViewport panPixels(double deltaX, double deltaZ) {
return new NoiseViewport(
centerX - (deltaX * blocksPerPixel),
centerZ - (deltaZ * blocksPerPixel),
blocksPerPixel
);
}
NoiseViewport zoomAt(double screenX, double screenZ, int width, int height, double factor) {
if (!Double.isFinite(factor) || factor <= 0D) {
throw new IllegalArgumentException("Zoom factor must be finite and positive");
}
double anchorX = worldX(screenX, width);
double anchorZ = worldZ(screenZ, height);
double nextScale = Math.max(MIN_BLOCKS_PER_PIXEL,
Math.min(MAX_BLOCKS_PER_PIXEL, blocksPerPixel * factor));
double nextCenterX = anchorX - ((screenX - (width / 2D)) * nextScale);
double nextCenterZ = anchorZ - ((screenZ - (height / 2D)) * nextScale);
return new NoiseViewport(nextCenterX, nextCenterZ, nextScale);
}
}
@@ -0,0 +1,270 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.ToolTipManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import java.awt.BorderLayout;
import java.awt.Component;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;
/**
* Every object in the pack as a folder tree, with a search that matches every word typed anywhere in an object's path.
*/
final class ObjectBrowser extends JPanel {
private static final long serialVersionUID = 6795434561028735214L;
private static final int ROW_HEIGHT = 24;
private final Consumer<String> onObjectChosen;
private final JTextField searchField = new JTextField();
private final DefaultTreeModel treeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
private final JTree tree = new JTree(treeModel);
private final JLabel countLabel = new JLabel("Loading objects...");
private List<String> objectKeys = List.of();
private String chosenKey;
private boolean updatingSelection;
ObjectBrowser(Consumer<String> onObjectChosen) {
super(new BorderLayout());
this.onObjectChosen = onObjectChosen;
StudioToolbar.makeSearchField(searchField, "Search objects...");
searchField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent event) {
rebuildTree();
}
@Override
public void removeUpdate(DocumentEvent event) {
rebuildTree();
}
@Override
public void changedUpdate(DocumentEvent event) {
rebuildTree();
}
});
tree.setRootVisible(false);
tree.setShowsRootHandles(true);
tree.setRowHeight(StudioTheme.scale(ROW_HEIGHT));
tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
tree.setCellRenderer(new EntryRenderer());
ToolTipManager.sharedInstance().registerComponent(tree);
tree.addTreeSelectionListener(event -> {
if (!updatingSelection && nodeAt(event.getPath()) instanceof Entry entry && entry.isObject()) {
chosenKey = entry.key();
onObjectChosen.accept(entry.key());
}
});
countLabel.setFont(StudioTheme.smallFont());
countLabel.setForeground(StudioTheme.SECONDARY_TEXT);
add(StudioToolbar.padded(searchField, 8, 8, 8, 8), BorderLayout.NORTH);
add(StickyFolderHeaders.around(tree, StudioToolbar.scroll(tree)), BorderLayout.CENTER);
add(StudioToolbar.padded(countLabel, 6, 12, 8, 12), BorderLayout.SOUTH);
}
void showObjects(List<String> keys) {
objectKeys = keys.stream().sorted(Comparator.comparing(key -> key.toLowerCase(Locale.ROOT))).toList();
countLabel.setText(objectKeys.size() == 1 ? "1 object" : objectKeys.size() + " objects");
rebuildTree();
}
void showError(String text) {
countLabel.setText(text);
}
String firstObject() {
return objectKeys.isEmpty() ? null : objectKeys.get(0);
}
private void rebuildTree() {
String[] words = searchField.getText().trim().toLowerCase(Locale.ROOT).split("\\s+");
boolean searching = !searchField.getText().isBlank();
DefaultMutableTreeNode root = searching ? searchResults(words) : folderTree();
treeModel.setRoot(root);
if (searching) {
expandAll();
}
selectChosen();
}
private DefaultMutableTreeNode searchResults(String[] words) {
DefaultMutableTreeNode root = new DefaultMutableTreeNode();
for (String key : objectKeys) {
if (matchesEveryWord(key, words)) {
root.add(new DefaultMutableTreeNode(Entry.object(key, key)));
}
}
if (root.getChildCount() == 0) {
root.add(new DefaultMutableTreeNode(Entry.note("No matching objects")));
}
return root;
}
private static boolean matchesEveryWord(String key, String[] words) {
String lowerKey = key.toLowerCase(Locale.ROOT);
for (String word : words) {
if (!lowerKey.contains(word)) {
return false;
}
}
return true;
}
// Folders come before the objects beside them, like a file browser.
private DefaultMutableTreeNode folderTree() {
Folder top = new Folder();
for (String key : objectKeys) {
Folder folder = top;
String[] parts = key.split("/");
for (int index = 0; index < parts.length - 1; index++) {
folder = folder.subfolders.computeIfAbsent(parts[index], name -> new Folder());
}
folder.objects.add(key);
}
DefaultMutableTreeNode root = new DefaultMutableTreeNode();
addFolderContents(root, top, "");
return root;
}
private static void addFolderContents(DefaultMutableTreeNode node, Folder folder, String path) {
for (Map.Entry<String, Folder> subfolder : folder.subfolders.entrySet()) {
String subfolderPath = path + subfolder.getKey() + "/";
DefaultMutableTreeNode child = new DefaultMutableTreeNode(Entry.folder(subfolder.getKey(), subfolderPath));
addFolderContents(child, subfolder.getValue(), subfolderPath);
node.add(child);
}
for (String key : folder.objects) {
node.add(new DefaultMutableTreeNode(Entry.object(key.substring(key.lastIndexOf('/') + 1), key)));
}
}
void select(String key) {
chosenKey = key;
selectChosen();
}
private void selectChosen() {
DefaultMutableTreeNode node = chosenKey == null ? null : findObjectNode((DefaultMutableTreeNode) treeModel.getRoot());
updatingSelection = true;
try {
if (node == null) {
tree.clearSelection();
return;
}
TreePath path = new TreePath(node.getPath());
tree.setSelectionPath(path);
tree.scrollPathToVisible(path);
} finally {
updatingSelection = false;
}
}
private DefaultMutableTreeNode findObjectNode(DefaultMutableTreeNode node) {
if (node.getUserObject() instanceof Entry entry && entry.isObject() && entry.key().equals(chosenKey)) {
return node;
}
for (int index = 0; index < node.getChildCount(); index++) {
DefaultMutableTreeNode found = findObjectNode((DefaultMutableTreeNode) node.getChildAt(index));
if (found != null) {
return found;
}
}
return null;
}
private void expandAll() {
for (int row = 0; row < tree.getRowCount(); row++) {
tree.expandRow(row);
}
}
private static Object nodeAt(TreePath path) {
return path == null ? null : ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
}
private static final class Folder {
private final Map<String, Folder> subfolders = new LinkedHashMap<>();
private final List<String> objects = new ArrayList<>();
}
/**
* A row of the tree: an object with its key, a folder with its path, or a note without either.
*/
private record Entry(String label, String key, String folderPath) {
static Entry object(String label, String key) {
return new Entry(label, key, null);
}
static Entry folder(String label, String path) {
return new Entry(label, null, path);
}
static Entry note(String text) {
return new Entry(text, null, null);
}
boolean isObject() {
return key != null;
}
@Override
public String toString() {
return label;
}
}
private static final class EntryRenderer extends DefaultTreeCellRenderer {
private static final long serialVersionUID = -4316652201793418740L;
private EntryRenderer() {
setLeafIcon(null);
setOpenIcon(null);
setClosedIcon(null);
}
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded,
boolean leaf, int row, boolean focus) {
Object userObject = ((DefaultMutableTreeNode) value).getUserObject();
super.getTreeCellRendererComponent(tree, userObject == null ? "" : userObject.toString(), selected, expanded, leaf, row, false);
boolean isNote = userObject instanceof Entry entry && !entry.isObject() && entry.folderPath() == null;
setFont(StudioTheme.font());
setForeground(isNote ? StudioTheme.MUTED_TEXT : StudioTheme.TEXT);
setToolTipText(userObject instanceof Entry entry ? entry.isObject() ? entry.key() : entry.folderPath() : null);
return this;
}
}
}
@@ -0,0 +1,44 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
/**
* Where the object viewer looks from: turned by yaw around the vertical axis, tilted down by pitch, both in radians.
* Zoom 1 fits the object in the view at the starting angle. Pan moves the picture in fractions of the view's size.
*/
record ObjectCamera(double yaw, double pitch, double zoom, double panX, double panY) {
static final ObjectCamera START = new ObjectCamera(0.76D, 0.38D, 1.72D, 0D, 0.025D);
private static final double RADIANS_PER_DRAGGED_PIXEL = 0.008D;
private static final double MAXIMUM_PITCH = 1.3D;
private static final double MINIMUM_ZOOM = 0.4D;
private static final double MAXIMUM_ZOOM = 12D;
ObjectCamera orbitedBy(double draggedX, double draggedY) {
double nextPitch = Math.max(-MAXIMUM_PITCH, Math.min(MAXIMUM_PITCH, pitch + draggedY * RADIANS_PER_DRAGGED_PIXEL));
return new ObjectCamera(yaw + draggedX * RADIANS_PER_DRAGGED_PIXEL, nextPitch, zoom, panX, panY);
}
ObjectCamera zoomedBy(double factor) {
return new ObjectCamera(yaw, pitch, Math.max(MINIMUM_ZOOM, Math.min(MAXIMUM_ZOOM, zoom * factor)), panX, panY);
}
ObjectCamera pannedBy(double fractionX, double fractionY) {
return new ObjectCamera(yaw, pitch, zoom, panX + fractionX, panY + fractionY);
}
}
@@ -0,0 +1,317 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisObject;
import org.bukkit.NamespacedKey;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.BlockVector;
import javax.swing.JComponent;
import java.awt.EventQueue;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
/**
* The map window's Objects view: every object in the pack in a folder tree, the chosen one in 3D, and what it is made
* of. Inspired by the Procedural Objects workbench.
*/
final class ObjectExplorer {
private static final NamespacedKey TINT_BIOME = NamespacedKey.minecraft("plains");
private static final int UNKNOWN_BLOCK_COLOR = 0x8A8A8A;
private static final String CAMERA_HINT = "Drag to orbit · Shift-drag to pan · Scroll to zoom · Page Up/Down cuts layers · R resets";
private final Engine startEngine;
private final UUID worldId;
private final ObjectBrowser browser = new ObjectBrowser(this::showObject);
private final ObjectViewport viewport = new ObjectViewport();
private final ObjectLayerCut layerCut = new ObjectLayerCut(shownLayers -> showLayerCut());
private final JComponent viewWithLayerCut = layerCut.floatingOver(viewport);
private final ObjectOverviewPanel overview = new ObjectOverviewPanel(this::showSeeThrough);
private final StudioStatusBar statusBar = new StudioStatusBar();
private final ExecutorService loader = Executors.newSingleThreadExecutor(runnable -> {
Thread thread = new Thread(runnable, "Iris Object Loader");
thread.setDaemon(true);
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
});
private final AtomicLong objectRevision = new AtomicLong();
private final AtomicLong meshRevision = new AtomicLong();
private final Map<String, BlockModel> models = new ConcurrentHashMap<>();
private final Runnable hotloadHook = this::reloadPack;
private volatile BlockTextures textures;
private volatile BiomeTintColors tintColors;
private String shownKey;
private LoadedObject shown;
private ObjectMesh wholeMesh;
private Set<String> seeThroughTypes = Set.of();
ObjectExplorer(Engine engine, UUID worldId) {
this.startEngine = engine;
this.worldId = worldId;
GuiHost.interruptOnPluginShutdown(loader);
GuiHost.registerHotloadHook(hotloadHook);
statusBar.showPosition(CAMERA_HINT);
viewport.bindKey(KeyEvent.VK_PAGE_UP, "show-layer-above", () -> layerCut.stepBy(1));
viewport.bindKey(KeyEvent.VK_PAGE_DOWN, "hide-top-layer", () -> layerCut.stepBy(-1));
}
JComponent browser() {
return browser;
}
ObjectViewport viewport() {
return viewport;
}
/**
* The 3D view with the slider that cuts away its top layers.
*/
JComponent view() {
return viewWithLayerCut;
}
JComponent overview() {
return overview;
}
StudioStatusBar statusBar() {
return statusBar;
}
void resetCamera() {
viewport.resetCamera();
}
// Textures load first, since the first object would otherwise be drawn in flat colours.
void open() {
statusBar.showActivity("Loading Minecraft textures...");
loader.execute(() -> {
loadTextures();
List<String> keys = objectKeys();
EventQueue.invokeLater(() -> showCatalog(keys));
});
}
void close() {
GuiHost.unregisterHotloadHook(hotloadHook);
objectRevision.incrementAndGet();
loader.shutdownNow();
viewport.close();
}
private void loadTextures() {
try {
BlockTextures loaded = BlockTextures.shared().join();
tintColors = loaded.biomeColors().vanilla(TINT_BIOME);
textures = loaded;
} catch (RuntimeException unavailable) {
EventQueue.invokeLater(() -> statusBar.showError("Minecraft textures are unavailable, objects use plain colours"));
}
}
private List<String> objectKeys() {
return Arrays.asList(engine().getData().getObjectLoader().getPossibleKeys());
}
private void showCatalog(List<String> keys) {
statusBar.showActivity("");
browser.showObjects(keys);
String key = shownKey != null && keys.contains(shownKey) ? shownKey : browser.firstObject();
if (key == null) {
viewport.showMessage("This pack has no objects");
overview.clear();
layerCut.showHeight(1);
return;
}
browser.select(key);
showObject(key);
}
private void showObject(String key) {
shownKey = key;
shown = null;
wholeMesh = null;
meshRevision.incrementAndGet();
long revision = objectRevision.incrementAndGet();
viewport.showMessage("Loading " + key + "...");
statusBar.showActivity("Loading " + key + "...");
loader.execute(() -> {
if (revision != objectRevision.get()) {
return;
}
try {
LoadedObject loaded = load(key);
EventQueue.invokeLater(() -> showLoaded(revision, loaded));
} catch (RuntimeException error) {
EventQueue.invokeLater(() -> showFailure(revision, key, error));
}
});
}
private LoadedObject load(String key) {
IrisObject object = engine().getData().getObjectLoader().load(key);
if (object == null) {
throw new IllegalStateException("The object file could not be read");
}
Map<BlockVector, BlockData> blocks = new HashMap<>();
object.getBlocks().forEach((position, block) -> {
if (!block.getMaterial().isAir()) {
blocks.put(position, block);
}
});
ObjectMesh mesh = ObjectMesh.build(blocks, this::modelOf);
ObjectSummary summary = ObjectSummary.of(key, mesh.bounds(), blocks.values(), this::iconOf);
return new LoadedObject(blocks, mesh, summary);
}
private void showLoaded(long revision, LoadedObject loaded) {
if (revision != objectRevision.get()) {
return;
}
shown = loaded;
wholeMesh = loaded.mesh();
seeThroughTypes = Set.of();
statusBar.showActivity("");
viewport.show(loaded.mesh());
overview.show(loaded.summary());
layerCut.showHeight(loaded.mesh().bounds().sizeY());
showMeshSummary(loaded.mesh());
}
private void showMeshSummary(ObjectMesh mesh) {
ObjectSummary summary = shown.summary();
String cut = layerCut.isCutting()
? String.format(Locale.ROOT, " · showing layers 1-%d of %d", layerCut.shownLayerCount(), layerCut.layerCount())
: "";
statusBar.showSummary(String.format(Locale.ROOT, "%s · %,d blocks · %,d faces drawn%s",
summary.key(), summary.blockCount(), mesh.faceCount(), cut));
}
private void showSeeThrough(Set<String> blockTypes) {
seeThroughTypes = blockTypes;
rebuildWholeMesh();
}
// Blocks behind see-through ones get their hidden faces back, so the mesh is built again.
private void rebuildWholeMesh() {
LoadedObject object = shown;
if (object == null) {
return;
}
long revision = meshRevision.incrementAndGet();
Set<String> seeThrough = seeThroughTypes;
loader.execute(() -> {
if (revision != meshRevision.get()) {
return;
}
ObjectMesh mesh = seeThrough.isEmpty()
? object.mesh()
: ObjectMesh.build(object.blocks(), this::modelOf, object.mesh().bounds(),
block -> seeThrough.contains(block.getMaterial().getKey().getKey()));
EventQueue.invokeLater(() -> {
if (revision == meshRevision.get() && shown == object) {
wholeMesh = mesh;
showLayerCut();
}
});
});
}
private void showLayerCut() {
if (wholeMesh == null) {
return;
}
int highestShownY = wholeMesh.bounds().minY() + layerCut.shownLayerCount() - 1;
ObjectMesh mesh = layerCut.isCutting() ? wholeMesh.cutAbove(highestShownY) : wholeMesh;
viewport.replaceMesh(mesh);
showMeshSummary(mesh);
}
private void showFailure(long revision, String key, Throwable error) {
if (revision != objectRevision.get()) {
return;
}
statusBar.showActivity("");
viewport.showMessage("Could not load " + key);
overview.clear();
layerCut.showHeight(1);
statusBar.showError(error.getMessage() == null ? error.getClass().getSimpleName() : error.getMessage());
}
// The pack's files may have changed, so the list and the shown object are read again.
private void reloadPack() {
loader.execute(() -> {
List<String> keys = objectKeys();
EventQueue.invokeLater(() -> showCatalog(keys));
});
}
private Engine engine() {
Engine current = GuiHost.findEngine(worldId);
return current != null && !current.isClosed() ? current : startEngine;
}
private BlockModel modelOf(BlockData block) {
return models.computeIfAbsent(block.getAsString(false), state -> {
BlockTextures available = textures;
if (available == null) {
return BlockModel.solidColor(UNKNOWN_BLOCK_COLOR);
}
return available.model(block).map(this::withBiomeTint).orElseGet(() -> BlockModel.solidColor(UNKNOWN_BLOCK_COLOR));
});
}
private BufferedImage iconOf(BlockData block) {
BlockTextures available = textures;
if (available == null) {
return null;
}
return available.iconTexture(block).map(this::withBiomeTint).map(BlockTexture::toImage).orElse(null);
}
// Objects have no biome of their own, so grass and leaves get plains' colours.
private BlockModel withBiomeTint(BlockModel model) {
return model.withTextures(this::withBiomeTint);
}
private BlockTexture withBiomeTint(BlockTexture texture) {
BiomeTintColors colors = tintColors;
if (texture.biomeTint() == null || colors == null) {
return texture;
}
return texture.tinted(colors.colorAt(texture.biomeTint(), 0, 0));
}
private record LoadedObject(Map<BlockVector, BlockData> blocks, ObjectMesh mesh, ObjectSummary summary) {
}
}
@@ -0,0 +1,179 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.SwingConstants;
import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.function.IntConsumer;
/**
* A slim slider floating on the right of the 3D view that hides an object's top layers, to look inside buildings
* floor by floor. Most objects never need it, so it stays faint until the mouse comes near or a cut is active.
*/
final class ObjectLayerCut extends JPanel {
private static final long serialVersionUID = 4473820211915526120L;
private static final int PANEL_WIDTH = 30;
private static final int MAX_HEIGHT = 320;
private static final double HEIGHT_SHARE_OF_VIEW = 0.5D;
private static final int EDGE_MARGIN = 8;
private static final float FAINT_OPACITY = 0.28F;
private final JSlider slider = new JSlider(SwingConstants.VERTICAL, 1, 1, 1);
private final JLabel shownLayers = new JLabel(" ", SwingConstants.CENTER);
private final IntConsumer onLayersChanged;
private boolean resetting;
private boolean hovered;
/**
* @param onLayersChanged told how many layers from the bottom stay visible whenever the user moves the slider
*/
ObjectLayerCut(IntConsumer onLayersChanged) {
super(new BorderLayout(0, StudioTheme.scale(4)));
this.onLayersChanged = onLayersChanged;
setOpaque(false);
setToolTipText("Hide the top layers to look inside · Page Up / Page Down");
shownLayers.setFont(StudioTheme.smallFont());
shownLayers.setForeground(StudioTheme.SECONDARY_TEXT);
slider.setOpaque(false);
slider.setFocusable(false);
slider.setToolTipText(getToolTipText());
slider.addChangeListener(event -> {
showLayerLabel();
if (!resetting) {
onLayersChanged.accept(slider.getValue());
}
});
MouseAdapter hover = new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent event) {
setHovered(true);
}
@Override
public void mouseExited(MouseEvent event) {
setHovered(slider.getValueIsAdjusting());
}
@Override
public void mouseReleased(MouseEvent event) {
setHovered(slider.contains(event.getPoint()));
}
};
slider.addMouseListener(hover);
addMouseListener(hover);
add(shownLayers, BorderLayout.NORTH);
add(slider, BorderLayout.CENTER);
showHeight(1);
}
/**
* The view with this slider floating over its right edge, halfway down.
*/
JComponent floatingOver(JComponent view) {
ObjectLayerCut cut = this;
JPanel stack = new JPanel(null) {
private static final long serialVersionUID = -1950381405658102347L;
// The slider overlaps the view, so the view may not repaint without the slider on top of it.
@Override
public boolean isOptimizedDrawingEnabled() {
return false;
}
@Override
public void doLayout() {
view.setBounds(0, 0, getWidth(), getHeight());
int width = StudioTheme.scale(PANEL_WIDTH);
int height = Math.min(StudioTheme.scale(MAX_HEIGHT), (int) (getHeight() * HEIGHT_SHARE_OF_VIEW));
cut.setBounds(getWidth() - width - StudioTheme.scale(EDGE_MARGIN), (getHeight() - height) / 2, width, height);
}
};
stack.add(cut);
stack.add(view);
return stack;
}
/**
* Shows every layer of a newly chosen object without telling anyone, since nothing is hidden yet.
*/
void showHeight(int layers) {
resetting = true;
try {
slider.setMaximum(Math.max(1, layers));
slider.setValue(slider.getMaximum());
} finally {
resetting = false;
}
showLayerLabel();
setVisible(layers > 1);
}
void stepBy(int layers) {
if (isVisible()) {
slider.setValue(slider.getValue() + layers);
}
}
boolean isCutting() {
return slider.getValue() < slider.getMaximum();
}
int shownLayerCount() {
return slider.getValue();
}
int layerCount() {
return slider.getMaximum();
}
@Override
protected void paintChildren(Graphics graphics) {
Graphics2D canvas = (Graphics2D) graphics.create();
try {
if (!hovered && !isCutting()) {
canvas.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, FAINT_OPACITY));
}
super.paintChildren(canvas);
} finally {
canvas.dispose();
}
}
private void setHovered(boolean nowHovered) {
if (hovered != nowHovered) {
hovered = nowHovered;
repaint();
}
}
private void showLayerLabel() {
shownLayers.setText(isCutting() ? Integer.toString(slider.getValue()) : " ");
repaint();
}
}
@@ -0,0 +1,243 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.core.gui.BlockModel.Face;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.BlockVector;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* The faces of an object's blocks that can be seen from outside. A face is left out when the neighbour on its side
* covers that side, which is most of them in a large object. See-through faces, like water and stained glass, are
* kept apart because they are drawn last, blended over everything behind them. Top faces covered only by the block
* above are kept aside too, so the top layers can be cut away without building the mesh again; what is cut away stays
* as a faint ghost of the object's shape.
*/
record ObjectMesh(List<Quad> solidQuads, List<Quad> translucentQuads, List<Quad> cutAwayQuads, List<Quad> topsUnderBlockAbove,
Bounds bounds) {
private static final double SEE_THROUGH_OPACITY = 0.22D;
private static final double FULL_OPACITY = 1D;
private static final double CUT_AWAY_OPACITY = 0.1D;
static ObjectMesh build(Map<BlockVector, BlockData> blocks, Function<BlockData, BlockModel> modelOf) {
return build(blocks, modelOf, Bounds.of(blocks.keySet()), block -> false);
}
/**
* @param bounds the whole object's extent, kept when only part of it is drawn so the view does not jump
* @param isSeeThrough blocks drawn faintly, which hide nothing behind them, to look past them into the object
*/
static ObjectMesh build(Map<BlockVector, BlockData> blocks, Function<BlockData, BlockModel> modelOf, Bounds bounds,
Predicate<BlockData> isSeeThrough) {
List<Quad> solid = new ArrayList<>();
List<Quad> translucent = new ArrayList<>();
List<Quad> topsUnderBlockAbove = new ArrayList<>();
BlockGrid grid = new BlockGrid(blocks, modelOf, bounds, isSeeThrough);
for (BlockVector position : blocks.keySet()) {
int x = position.getBlockX();
int y = position.getBlockY();
int z = position.getBlockZ();
int cell = grid.cellAt(x, y, z);
for (Face face : grid.modelIn(cell).faces()) {
Quad quad = new Quad(face, x, y, z, grid.isSeeThroughIn(cell) ? SEE_THROUGH_OPACITY : FULL_OPACITY);
if (!grid.isHiddenByNeighbour(x, y, z, face)) {
(quad.isBlended() ? translucent : solid).add(quad);
} else if (face.cullFace() == CubeFace.UP) {
topsUnderBlockAbove.add(quad);
}
}
}
return new ObjectMesh(List.copyOf(solid), List.copyOf(translucent), List.of(), List.copyOf(topsUnderBlockAbove), bounds);
}
/**
* This mesh with the blocks above the given layer faded to a ghost of the object's shape, and the tops of that
* layer's blocks showing again. Quick enough to follow a slider as it moves.
*/
ObjectMesh cutAbove(int highestShownY) {
List<Quad> solid = new ArrayList<>();
List<Quad> translucent = new ArrayList<>();
List<Quad> cutAway = new ArrayList<>();
for (Quad quad : solidQuads) {
keepOrCutAway(quad, highestShownY, solid, cutAway);
}
for (Quad quad : translucentQuads) {
keepOrCutAway(quad, highestShownY, translucent, cutAway);
}
for (Quad quad : topsUnderBlockAbove) {
if (quad.y() == highestShownY) {
(quad.isBlended() ? translucent : solid).add(quad);
}
}
return new ObjectMesh(solid, translucent, cutAway, List.of(), bounds);
}
private static void keepOrCutAway(Quad quad, int highestShownY, List<Quad> kept, List<Quad> cutAway) {
if (quad.y() <= highestShownY) {
kept.add(quad);
} else {
cutAway.add(quad.faded(CUT_AWAY_OPACITY));
}
}
boolean isEmpty() {
return solidQuads.isEmpty() && translucentQuads.isEmpty() && cutAwayQuads.isEmpty();
}
int faceCount() {
return solidQuads.size() + translucentQuads.size();
}
/**
* The object's blocks laid out in an array over its bounds, so finding a block's neighbours is quick. Looking them
* up by {@link BlockVector} in a map is slow, since its hash codes collide a lot.
*/
private static final class BlockGrid {
private final Bounds bounds;
private final BlockData[] blocks;
private final BlockModel[] models;
private final boolean[] seeThrough;
BlockGrid(Map<BlockVector, BlockData> placed, Function<BlockData, BlockModel> modelOf, Bounds bounds,
Predicate<BlockData> isSeeThrough) {
this.bounds = bounds;
int cells = bounds.sizeX() * bounds.sizeY() * bounds.sizeZ();
this.blocks = new BlockData[cells];
this.models = new BlockModel[cells];
this.seeThrough = new boolean[cells];
placed.forEach((position, block) -> {
int cell = cellAt(position.getBlockX(), position.getBlockY(), position.getBlockZ());
blocks[cell] = block;
models[cell] = modelOf.apply(block);
seeThrough[cell] = isSeeThrough.test(block);
});
}
int cellAt(int x, int y, int z) {
return ((y - bounds.minY()) * bounds.sizeZ() + (z - bounds.minZ())) * bounds.sizeX() + (x - bounds.minX());
}
BlockModel modelIn(int cell) {
return models[cell];
}
boolean isSeeThroughIn(int cell) {
return seeThrough[cell];
}
// Glass next to glass and water next to water hide the side between them too, as they do in the game. A
// see-through block only hides sides of other see-through blocks, so whatever it covers shows through it.
boolean isHiddenByNeighbour(int x, int y, int z, Face face) {
CubeFace side = face.cullFace();
if (side == null || !contains(x + side.stepX(), y + side.stepY(), z + side.stepZ())) {
return false;
}
int cell = cellAt(x, y, z);
int neighbourCell = cellAt(x + side.stepX(), y + side.stepY(), z + side.stepZ());
BlockData neighbour = blocks[neighbourCell];
if (neighbour == null || (seeThrough[neighbourCell] && !seeThrough[cell])) {
return false;
}
BlockModel neighbourModel = models[neighbourCell];
if (neighbourModel.coveredSides().contains(side.opposite())) {
return true;
}
return neighbour.getMaterial() == blocks[cell].getMaterial() && models[cell].hidesSameBlockSides()
&& neighbourModel.hidesSameBlockSides();
}
private boolean contains(int x, int y, int z) {
return x >= bounds.minX() && x < bounds.maxX() && y >= bounds.minY() && y < bounds.maxY()
&& z >= bounds.minZ() && z < bounds.maxZ();
}
}
/**
* A model face placed at a block in the object, drawn at the given opacity on top of its texture's own.
*/
record Quad(Face face, int x, int y, int z, double opacity) {
boolean isBlended() {
return opacity < FULL_OPACITY || face.texture().isTranslucent();
}
Quad faded(double factor) {
return new Quad(face, x, y, z, opacity * factor);
}
double originX() {
return x + face.origin().x();
}
double originY() {
return y + face.origin().y();
}
double originZ() {
return z + face.origin().z();
}
}
/**
* The blocks' extent, with the maximum one past the last block so a single block spans one unit.
*/
record Bounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
private static final Bounds EMPTY = new Bounds(0, 0, 0, 1, 1, 1);
static Bounds of(Iterable<BlockVector> positions) {
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
int minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxY = Integer.MIN_VALUE;
int maxZ = Integer.MIN_VALUE;
for (BlockVector position : positions) {
minX = Math.min(minX, position.getBlockX());
minY = Math.min(minY, position.getBlockY());
minZ = Math.min(minZ, position.getBlockZ());
maxX = Math.max(maxX, position.getBlockX() + 1);
maxY = Math.max(maxY, position.getBlockY() + 1);
maxZ = Math.max(maxZ, position.getBlockZ() + 1);
}
return minX == Integer.MAX_VALUE ? EMPTY : new Bounds(minX, minY, minZ, maxX, maxY, maxZ);
}
int sizeX() {
return maxX - minX;
}
int sizeY() {
return maxY - minY;
}
int sizeZ() {
return maxZ - minZ;
}
int largestSize() {
return Math.max(sizeX(), Math.max(sizeY(), sizeZ()));
}
}
}
@@ -0,0 +1,351 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.SwingUtilities;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Composite;
import java.awt.Cursor;
import java.awt.Insets;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.function.Consumer;
/**
* The chosen object's size and the blocks it is made of, most used first, like the overview in Procedural Objects.
* Clicking a block makes it see-through in the 3D view, to look past walls into the object.
*/
final class ObjectOverviewPanel extends JPanel {
private static final long serialVersionUID = -8811407613645718290L;
private static final int PANEL_WIDTH = 280;
private static final int ROW_HEIGHT = 26;
private static final int ICON_SIZE = 16;
private static final int ICON_GAP = 8;
private static final int ROW_PADDING = 10;
private static final int SHARE_WIDTH = 92;
private static final int SHARE_BAR_HEIGHT = 4;
private static final float SEE_THROUGH_ICON_OPACITY = 0.3F;
private static final String SEE_THROUGH_HINT = "Click a block to see through it";
private final JLabel title = new JLabel(" ");
private final JLabel path = new JLabel(" ");
private final JLabel size = statValue();
private final JLabel blockCount = statValue();
private final JLabel blockTypes = statValue();
private final JLabel filled = statValue();
private final JTextField filterField = new JTextField();
private final DefaultListModel<ObjectSummary.BlockUse> rows = new DefaultListModel<>();
private final JList<ObjectSummary.BlockUse> list = new JList<>(rows) {
@Override
public boolean getScrollableTracksViewportWidth() {
return true;
}
};
private final JLabel seeThroughHint = new JLabel(SEE_THROUGH_HINT);
private final JButton showAllSolid = new JButton("Show all solid");
private final Set<String> seeThroughTypes = new LinkedHashSet<>();
private final Consumer<Set<String>> onSeeThroughChanged;
private ObjectSummary summary;
/**
* @param onSeeThroughChanged told which block types to draw see-through whenever the user changes them
*/
ObjectOverviewPanel(Consumer<Set<String>> onSeeThroughChanged) {
super(new BorderLayout());
this.onSeeThroughChanged = onSeeThroughChanged;
setPreferredSize(StudioTheme.scaledSize(PANEL_WIDTH, 0));
setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, StudioTheme.SEPARATOR));
setBackground(StudioTheme.PANEL_BACKGROUND);
add(buildHeader(), BorderLayout.NORTH);
add(buildBlockList(), BorderLayout.CENTER);
}
void show(ObjectSummary nextSummary) {
summary = nextSummary;
title.setText(nextSummary.name());
path.setText(nextSummary.key());
path.setToolTipText(nextSummary.key());
size.setText(nextSummary.sizeX() + " × " + nextSummary.sizeY() + " × " + nextSummary.sizeZ());
blockCount.setText(formatCount(nextSummary.blockCount()));
blockTypes.setText(formatCount(nextSummary.blockUses().size()));
filled.setText(String.format(Locale.ROOT, "%.1f%%", nextSummary.filledFraction() * 100D));
seeThroughTypes.clear();
showSeeThroughState();
refilter();
}
void clear() {
summary = null;
title.setText(" ");
path.setText(" ");
for (JLabel value : List.of(size, blockCount, blockTypes, filled)) {
value.setText("—");
}
rows.clear();
seeThroughTypes.clear();
showSeeThroughState();
}
private JPanel buildHeader() {
JPanel header = new JPanel();
header.setLayout(new BoxLayout(header, BoxLayout.Y_AXIS));
header.setOpaque(false);
header.setBorder(StudioTheme.emptyBorder(12, 16, 8, 16));
title.setFont(StudioTheme.titleFont());
path.setFont(StudioTheme.smallFont());
path.setForeground(StudioTheme.SECONDARY_TEXT);
header.add(leftAligned(title));
header.add(leftAligned(path));
JPanel stats = new JPanel(new GridLayout(2, 2, StudioTheme.scale(12), StudioTheme.scale(8)));
stats.setOpaque(false);
stats.setBorder(StudioTheme.emptyBorder(12, 0, 4, 0));
stats.add(stat("Size · X × Y × Z", size));
stats.add(stat("Blocks", blockCount));
stats.add(stat("Block types", blockTypes));
stats.add(stat("Filled volume", filled));
filled.setToolTipText("How much of the object's bounding box its blocks fill");
header.add(leftAligned(stats));
clear();
return header;
}
private JPanel buildBlockList() {
JPanel blocks = new JPanel(new BorderLayout());
blocks.setOpaque(false);
StudioToolbar.makeSearchField(filterField, "Find a block...");
filterField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent event) {
refilter();
}
@Override
public void removeUpdate(DocumentEvent event) {
refilter();
}
@Override
public void changedUpdate(DocumentEvent event) {
refilter();
}
});
list.setFocusable(false);
list.setFixedCellHeight(StudioTheme.scale(ROW_HEIGHT));
list.setCellRenderer(new BlockUseRenderer());
list.setBackground(StudioTheme.PANEL_BACKGROUND);
list.setBorder(StudioTheme.emptyBorder(0, 6, 4, 6));
list.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
list.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent event) {
int row = list.locationToIndex(event.getPoint());
if (SwingUtilities.isLeftMouseButton(event) && row >= 0 && list.getCellBounds(row, row).contains(event.getPoint())) {
toggleSeeThrough(rows.get(row).blockType());
}
}
});
JPanel top = new JPanel();
top.setLayout(new BoxLayout(top, BoxLayout.Y_AXIS));
top.setOpaque(false);
top.add(leftAligned(StudioToolbar.padded(filterField, 4, 12, 4, 12)));
top.add(leftAligned(StudioToolbar.padded(seeThroughRow(), 0, 14, 6, 12)));
blocks.add(top, BorderLayout.NORTH);
blocks.add(StudioToolbar.scroll(list), BorderLayout.CENTER);
return blocks;
}
private JPanel seeThroughRow() {
seeThroughHint.setFont(StudioTheme.smallFont());
seeThroughHint.setForeground(StudioTheme.MUTED_TEXT);
showAllSolid.setFont(StudioTheme.smallFont());
showAllSolid.setForeground(StudioTheme.LINK);
showAllSolid.setBorderPainted(false);
showAllSolid.setContentAreaFilled(false);
showAllSolid.setFocusable(false);
showAllSolid.setMargin(new Insets(0, 0, 0, 0));
showAllSolid.setBorder(BorderFactory.createEmptyBorder());
showAllSolid.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
showAllSolid.addActionListener(event -> {
seeThroughTypes.clear();
seeThroughChanged();
});
JPanel row = new JPanel(new BorderLayout());
row.setOpaque(false);
row.add(seeThroughHint, BorderLayout.CENTER);
row.add(showAllSolid, BorderLayout.EAST);
showSeeThroughState();
return row;
}
private void toggleSeeThrough(String blockType) {
if (!seeThroughTypes.remove(blockType)) {
seeThroughTypes.add(blockType);
}
seeThroughChanged();
}
private void seeThroughChanged() {
showSeeThroughState();
list.repaint();
onSeeThroughChanged.accept(Set.copyOf(seeThroughTypes));
}
private void showSeeThroughState() {
int count = seeThroughTypes.size();
seeThroughHint.setText(count == 0 ? SEE_THROUGH_HINT : count == 1 ? "1 block see-through" : count + " blocks see-through");
showAllSolid.setVisible(count > 0);
}
private void refilter() {
rows.clear();
if (summary == null) {
return;
}
String filter = filterField.getText().trim().toLowerCase(Locale.ROOT).replace(' ', '_');
for (ObjectSummary.BlockUse use : summary.blockUses()) {
if (use.blockType().contains(filter)) {
rows.addElement(use);
}
}
}
private static JPanel stat(String label, JLabel value) {
JPanel stat = new JPanel(new BorderLayout());
stat.setOpaque(false);
JLabel name = new JLabel(label);
name.setFont(StudioTheme.smallFont());
name.setForeground(StudioTheme.MUTED_TEXT);
stat.add(name, BorderLayout.NORTH);
stat.add(value, BorderLayout.CENTER);
return stat;
}
private static JLabel statValue() {
JLabel value = new JLabel("—");
value.setFont(StudioTheme.boldFont());
return value;
}
private static Component leftAligned(JPanel panel) {
panel.setAlignmentX(LEFT_ALIGNMENT);
return panel;
}
private static Component leftAligned(JLabel label) {
label.setAlignmentX(LEFT_ALIGNMENT);
return label;
}
private static String formatCount(int count) {
return String.format(Locale.ROOT, "%,d", count);
}
private final class BlockUseRenderer extends DefaultListCellRenderer {
private static final long serialVersionUID = 2785337045812613941L;
private transient BufferedImage icon;
private boolean seeThrough;
private double share;
private String percent = "";
@Override
public Component getListCellRendererComponent(JList<?> owner, Object value, int index, boolean selected, boolean focused) {
ObjectSummary.BlockUse use = (ObjectSummary.BlockUse) value;
super.getListCellRendererComponent(owner, use.blockType(), index, false, false);
seeThrough = seeThroughTypes.contains(use.blockType());
setFont(StudioTheme.font());
setForeground(seeThrough ? StudioTheme.MUTED_TEXT : StudioTheme.TEXT);
setBorder(StudioTheme.emptyBorder(0, ROW_PADDING + ICON_SIZE + ICON_GAP, 0, ROW_PADDING + SHARE_WIDTH));
setToolTipText(use.blockType() + " · " + formatCount(use.count()) + " blocks · click to "
+ (seeThrough ? "make solid again" : "see through it"));
icon = use.icon();
share = summary == null || summary.blockCount() == 0 ? 0D : use.count() / (double) summary.blockCount();
percent = share > 0D && share < 0.001D ? "<0.1%" : String.format(Locale.ROOT, "%.1f%%", share * 100D);
return this;
}
@Override
protected void paintComponent(Graphics graphics) {
super.paintComponent(graphics);
Graphics2D canvas = (Graphics2D) graphics.create();
try {
int iconSize = StudioTheme.scale(ICON_SIZE);
int iconX = StudioTheme.scale(ROW_PADDING);
int iconY = (getHeight() - iconSize) / 2;
if (icon != null) {
canvas.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
Composite solid = canvas.getComposite();
if (seeThrough) {
canvas.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, SEE_THROUGH_ICON_OPACITY));
}
canvas.drawImage(icon, iconX, iconY, iconSize, iconSize, null);
canvas.setComposite(solid);
} else {
canvas.setColor(StudioTheme.CONTROL_BORDER);
canvas.drawRect(iconX, iconY, iconSize - 1, iconSize - 1);
}
drawShare(canvas);
} finally {
canvas.dispose();
}
}
private void drawShare(Graphics2D canvas) {
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
canvas.setFont(StudioTheme.smallFont());
int padding = StudioTheme.scale(ROW_PADDING);
int percentWidth = canvas.getFontMetrics().stringWidth("100.0%");
int barWidth = StudioTheme.scale(SHARE_WIDTH) - percentWidth - StudioTheme.scale(6);
int barX = getWidth() - padding - StudioTheme.scale(SHARE_WIDTH);
int barHeight = StudioTheme.scale(SHARE_BAR_HEIGHT);
int barY = (getHeight() - barHeight) / 2;
canvas.setColor(StudioTheme.HOVER_BACKGROUND);
canvas.fillRoundRect(barX, barY, barWidth, barHeight, barHeight, barHeight);
canvas.setColor(seeThrough ? StudioTheme.CONTROL_BORDER : StudioTheme.ACCENT);
canvas.fillRoundRect(barX, barY, Math.max(barHeight, (int) Math.round(barWidth * share)), barHeight, barHeight, barHeight);
canvas.setColor(StudioTheme.SECONDARY_TEXT);
int textWidth = canvas.getFontMetrics().stringWidth(percent);
int baseline = (getHeight() + canvas.getFontMetrics().getAscent() - canvas.getFontMetrics().getDescent()) / 2;
canvas.drawString(percent, getWidth() - padding - textWidth, baseline);
}
}
}
@@ -0,0 +1,331 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.core.gui.BlockModel.Face;
import com.volmit.iris.core.gui.BlockModel.TextureMapping;
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
/**
* Draws an object mesh with an orthographic camera into a picture, without a graphics card: each quad is filled pixel
* by pixel, and a depth buffer keeps the nearest one. A cut-away part's faint ghost only fills pixels the object
* leaves empty, so it never hazes over what is shown. See-through quads come last, farthest first, each blended over
* what is already drawn.
*/
final class ObjectRenderer {
private static final Color GRID = new ThemeColor(0xE3E4E8, 0x34373C);
private static final int GRID_LINES_PER_SIDE = 25;
private static final double GRID_REACH = 0.7D;
private static final double GRID_BELOW_OBJECT = 0.035D;
// Shared quad edges land exactly on pixel centres now and then; a little overlap keeps them from leaving cracks.
private static final double EDGE_TOLERANCE = 1e-6D;
private static final double EDGE_ON_DETERMINANT = 1e-9D;
// A face's own texels end a hair inside its UV rectangle, so rounding never picks the texel next to it.
private static final double TEXEL_EDGE_MARGIN = 1e-6D;
private static final double FULL_ALPHA = 255D;
private final int width;
private final int height;
private final int[] pixels;
private final double[] depths;
private final boolean[] showsObject;
private final Projection projection;
private final int gridColor = GRID.getRGB() & 0xFFFFFF;
private ObjectRenderer(ObjectMesh mesh, ObjectCamera camera, BufferedImage image, int coveredLeftWidth) {
this.width = image.getWidth();
this.height = image.getHeight();
this.pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
this.depths = new double[pixels.length];
this.showsObject = new boolean[pixels.length];
this.projection = new Projection(mesh.bounds(), camera, width, height, coveredLeftWidth);
}
/**
* @param coveredLeftWidth how much of the picture's left side something floats over, so the object is centred and
* fitted in the rest
*/
static BufferedImage render(ObjectMesh mesh, ObjectCamera camera, int width, int height, int coveredLeftWidth) {
BufferedImage image = new BufferedImage(Math.max(1, width), Math.max(1, height), BufferedImage.TYPE_INT_RGB);
ObjectRenderer renderer = new ObjectRenderer(mesh, camera, image, Math.max(0, Math.min(width - 1, coveredLeftWidth)));
Arrays.fill(renderer.pixels, StudioTheme.EDITOR_BACKGROUND.getRGB() & 0xFFFFFF);
Arrays.fill(renderer.depths, Double.NEGATIVE_INFINITY);
renderer.drawGrid(mesh.bounds());
for (ObjectMesh.Quad quad : mesh.solidQuads()) {
renderer.drawQuad(quad, Pass.SOLID);
}
for (ObjectMesh.Quad quad : mesh.cutAwayQuads()) {
renderer.drawQuad(quad, Pass.CUT_AWAY_GHOST);
}
for (ObjectMesh.Quad quad : renderer.farthestFirst(mesh.translucentQuads())) {
renderer.drawQuad(quad, Pass.BLENDED);
}
return image;
}
private List<ObjectMesh.Quad> farthestFirst(List<ObjectMesh.Quad> quads) {
List<ObjectMesh.Quad> sorted = new ArrayList<>(quads);
sorted.sort(Comparator.comparingDouble(this::centerDepth));
return sorted;
}
private double centerDepth(ObjectMesh.Quad quad) {
Face face = quad.face();
return projection.depth(
quad.originX() + (face.edgeU().x() + face.edgeV().x()) / 2D,
quad.originY() + (face.edgeU().y() + face.edgeV().y()) / 2D,
quad.originZ() + (face.edgeU().z() + face.edgeV().z()) / 2D);
}
private void drawGrid(ObjectMesh.Bounds bounds) {
int reach = (int) Math.ceil(bounds.largestSize() * GRID_REACH);
int step = Math.max(1, (int) Math.ceil(reach / (double) GRID_LINES_PER_SIDE));
double floor = bounds.minY() - GRID_BELOW_OBJECT;
int centerX = Math.floorDiv(bounds.minX() + bounds.maxX(), 2);
int centerZ = Math.floorDiv(bounds.minZ() + bounds.maxZ(), 2);
for (int offset = -reach; offset <= reach; offset += step) {
drawLine(centerX + offset, floor, centerZ - reach, centerX + offset, floor, centerZ + reach);
drawLine(centerX - reach, floor, centerZ + offset, centerX + reach, floor, centerZ + offset);
}
}
private void drawLine(double fromX, double fromY, double fromZ, double toX, double toY, double toZ) {
double startX = projection.screenX(fromX, fromY, fromZ);
double startY = projection.screenY(fromX, fromY, fromZ);
double startDepth = projection.depth(fromX, fromY, fromZ);
double endX = projection.screenX(toX, toY, toZ);
double endY = projection.screenY(toX, toY, toZ);
double endDepth = projection.depth(toX, toY, toZ);
int steps = (int) Math.ceil(Math.max(Math.abs(endX - startX), Math.abs(endY - startY)));
for (int step = 0; step <= steps; step++) {
double progress = steps == 0 ? 0D : step / (double) steps;
int x = (int) (startX + (endX - startX) * progress);
int y = (int) (startY + (endY - startY) * progress);
if (x < 0 || y < 0 || x >= width || y >= height) {
continue;
}
int index = y * width + x;
double depth = startDepth + (endDepth - startDepth) * progress;
if (depth > depths[index]) {
depths[index] = depth;
pixels[index] = gridColor;
}
}
}
// Later faces win ties, so an overlay drawn on the same box, like grass on a grass block's side, shows on top.
private void drawQuad(ObjectMesh.Quad quad, Pass pass) {
Face face = quad.face();
if (!projection.facesCamera(face.normal().x(), face.normal().y(), face.normal().z())) {
return;
}
Vec3 u = face.edgeU();
Vec3 v = face.edgeV();
double originX = projection.screenX(quad.originX(), quad.originY(), quad.originZ());
double originY = projection.screenY(quad.originX(), quad.originY(), quad.originZ());
double originDepth = projection.depth(quad.originX(), quad.originY(), quad.originZ());
double edgeUX = projection.screenStepX(u.x(), u.z());
double edgeUY = projection.screenStepY(u.x(), u.y(), u.z());
double edgeUDepth = projection.depthStep(u.x(), u.y(), u.z());
double edgeVX = projection.screenStepX(v.x(), v.z());
double edgeVY = projection.screenStepY(v.x(), v.y(), v.z());
double edgeVDepth = projection.depthStep(v.x(), v.y(), v.z());
double determinant = edgeUX * edgeVY - edgeUY * edgeVX;
if (Math.abs(determinant) < EDGE_ON_DETERMINANT) {
return;
}
int left = Math.max(0, (int) Math.floor(min4(originX, originX + edgeUX, originX + edgeVX, originX + edgeUX + edgeVX)));
int right = Math.min(width - 1, (int) Math.ceil(max4(originX, originX + edgeUX, originX + edgeVX, originX + edgeUX + edgeVX)));
int top = Math.max(0, (int) Math.floor(min4(originY, originY + edgeUY, originY + edgeVY, originY + edgeUY + edgeVY)));
int bottom = Math.min(height - 1, (int) Math.ceil(max4(originY, originY + edgeUY, originY + edgeVY, originY + edgeUY + edgeVY)));
if (left > right || top > bottom) {
return;
}
BlockTexture texture = face.texture();
TextureMapping mapping = face.mapping();
TexelRange columns = TexelRange.of(mapping.u(), mapping.uAlongU(), mapping.uAlongV(), texture.width());
TexelRange rows = TexelRange.of(mapping.v(), mapping.vAlongU(), mapping.vAlongV(), texture.height());
int[] texels = texture.pixels();
boolean[] opaque = texture.opaque();
int[] alphas = texture.alphas();
double shade = face.shade();
double alongUPerPixelX = edgeVY / determinant;
double alongVPerPixelX = -edgeUY / determinant;
for (int y = top; y <= bottom; y++) {
double offsetX = left + 0.5D - originX;
double offsetY = y + 0.5D - originY;
double alongU = (offsetX * edgeVY - offsetY * edgeVX) / determinant;
double alongV = (offsetY * edgeUX - offsetX * edgeUY) / determinant;
int rowStart = y * width;
for (int x = left; x <= right; x++, alongU += alongUPerPixelX, alongV += alongVPerPixelX) {
if (alongU < -EDGE_TOLERANCE || alongU > 1D + EDGE_TOLERANCE || alongV < -EDGE_TOLERANCE || alongV > 1D + EDGE_TOLERANCE) {
continue;
}
int index = rowStart + x;
double depth = originDepth + alongU * edgeUDepth + alongV * edgeVDepth;
if (depth < depths[index] || (pass == Pass.CUT_AWAY_GHOST && showsObject[index])) {
continue;
}
int texelX = columns.texelAt(mapping.u() + alongU * mapping.uAlongU() + alongV * mapping.uAlongV());
int texelY = rows.texelAt(mapping.v() + alongU * mapping.vAlongU() + alongV * mapping.vAlongV());
int texel = texelY * texture.width() + texelX;
if (!opaque[texel]) {
continue;
}
int color = BlockPreviewColors.shade(texels[texel], shade);
if (pass != Pass.SOLID) {
pixels[index] = blend(color, pixels[index], (alphas == null ? 1D : alphas[texel] / FULL_ALPHA) * quad.opacity());
continue;
}
depths[index] = depth;
pixels[index] = color;
showsObject[index] = true;
}
}
}
private static int blend(int over, int under, double coverage) {
return blendChannel(over, under, coverage, 16) << 16 | blendChannel(over, under, coverage, 8) << 8 | blendChannel(over, under, coverage, 0);
}
private static int blendChannel(int over, int under, double coverage, int shift) {
return (int) Math.round(((over >>> shift) & 0xFF) * coverage + ((under >>> shift) & 0xFF) * (1D - coverage));
}
private static double min4(double a, double b, double c, double d) {
return Math.min(Math.min(a, b), Math.min(c, d));
}
private static double max4(double a, double b, double c, double d) {
return Math.max(Math.max(a, b), Math.max(c, d));
}
/**
* The texels a face covers along one texture axis, so a pixel on the face's edge never takes its neighbour's.
*/
private record TexelRange(int first, int last, int size) {
static TexelRange of(double start, double alongU, double alongV, int size) {
double low = start + Math.min(0D, alongU) + Math.min(0D, alongV);
double high = start + Math.max(0D, alongU) + Math.max(0D, alongV);
int first = clamp((int) Math.floor(low * size + TEXEL_EDGE_MARGIN), size);
int last = Math.max(first, clamp((int) Math.ceil(high * size - TEXEL_EDGE_MARGIN) - 1, size));
return new TexelRange(first, last, size);
}
int texelAt(double fraction) {
int texel = (int) Math.floor(fraction * size);
return texel < first ? first : Math.min(texel, last);
}
private static int clamp(int texel, int size) {
return Math.max(0, Math.min(size - 1, texel));
}
}
/**
* Turns block positions into pixels. Larger depth is nearer the camera.
*/
private static final class Projection {
private final double centerX;
private final double centerY;
private final double centerZ;
private final double cosYaw;
private final double sinYaw;
private final double cosPitch;
private final double sinPitch;
private final double pixelsPerBlock;
private final double screenCenterX;
private final double screenCenterY;
private Projection(ObjectMesh.Bounds bounds, ObjectCamera camera, int width, int height, int coveredLeftWidth) {
this.centerX = (bounds.minX() + bounds.maxX()) / 2D;
this.centerY = (bounds.minY() + bounds.maxY()) / 2D;
this.centerZ = (bounds.minZ() + bounds.maxZ()) / 2D;
this.cosYaw = Math.cos(camera.yaw());
this.sinYaw = Math.sin(camera.yaw());
this.cosPitch = Math.cos(camera.pitch());
this.sinPitch = Math.sin(camera.pitch());
int visibleWidth = width - coveredLeftWidth;
double fit = fitAtStartingAngle(bounds, visibleWidth / (double) height);
this.pixelsPerBlock = camera.zoom() * fit * height / 2D / bounds.largestSize();
this.screenCenterX = coveredLeftWidth + visibleWidth / 2D + camera.panX() * visibleWidth / 2D;
this.screenCenterY = height / 2D - camera.panY() * height / 2D;
}
// How far to scale the object down so it fits the view at the starting angle; orbiting keeps the scale.
private static double fitAtStartingAngle(ObjectMesh.Bounds bounds, double aspect) {
double size = bounds.largestSize();
double width = bounds.sizeX() / size;
double height = bounds.sizeY() / size;
double depth = bounds.sizeZ() / size;
double yaw = ObjectCamera.START.yaw();
double pitch = ObjectCamera.START.pitch();
double projectedWidth = Math.abs(Math.cos(yaw)) * width + Math.abs(Math.sin(yaw)) * depth;
double projectedDepth = Math.abs(Math.sin(yaw)) * width + Math.abs(Math.cos(yaw)) * depth;
double projectedHeight = Math.abs(Math.cos(pitch)) * height + Math.abs(Math.sin(pitch)) * projectedDepth;
return Math.min(1D, Math.min(aspect / projectedWidth, 1D / projectedHeight));
}
double screenX(double x, double y, double z) {
return screenCenterX + screenStepX(x - centerX, z - centerZ);
}
double screenY(double x, double y, double z) {
return screenCenterY + screenStepY(x - centerX, y - centerY, z - centerZ);
}
double depth(double x, double y, double z) {
return depthStep(x - centerX, y - centerY, z - centerZ);
}
double screenStepX(double x, double z) {
return (cosYaw * x - sinYaw * z) * pixelsPerBlock;
}
double screenStepY(double x, double y, double z) {
double towardCamera = sinYaw * x + cosYaw * z;
return -(cosPitch * y - sinPitch * towardCamera) * pixelsPerBlock;
}
double depthStep(double x, double y, double z) {
double towardCamera = sinYaw * x + cosYaw * z;
return sinPitch * y + cosPitch * towardCamera;
}
boolean facesCamera(double normalX, double normalY, double normalZ) {
return depthStep(normalX, normalY, normalZ) > 0D;
}
}
private enum Pass {
SOLID,
CUT_AWAY_GHOST,
BLENDED
}
}
@@ -0,0 +1,65 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import org.bukkit.block.data.BlockData;
import java.awt.image.BufferedImage;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
/**
* What an object is made of: its size and how many of each block type it uses, most used first.
*/
record ObjectSummary(String key, int sizeX, int sizeY, int sizeZ, int blockCount, List<BlockUse> blockUses) {
static ObjectSummary of(String key, ObjectMesh.Bounds bounds, Iterable<BlockData> blocks, Function<BlockData, BufferedImage> iconOf) {
Map<String, Integer> counts = new HashMap<>();
Map<String, BlockData> examples = new HashMap<>();
int blockCount = 0;
for (BlockData block : blocks) {
String blockType = block.getMaterial().getKey().getKey();
counts.merge(blockType, 1, Integer::sum);
examples.putIfAbsent(blockType, block);
blockCount++;
}
List<BlockUse> uses = counts.entrySet().stream()
.map(count -> new BlockUse(count.getKey(), count.getValue(), iconOf.apply(examples.get(count.getKey()))))
.sorted(Comparator.comparingInt(BlockUse::count).reversed().thenComparing(BlockUse::blockType))
.toList();
return new ObjectSummary(key, bounds.sizeX(), bounds.sizeY(), bounds.sizeZ(), blockCount, uses);
}
String name() {
return key.substring(key.lastIndexOf('/') + 1);
}
double filledFraction() {
long volume = (long) sizeX * sizeY * sizeZ;
return volume == 0 ? 0D : blockCount / (double) volume;
}
/**
* @param icon the block's texture, or null when Minecraft's textures are not available
*/
record BlockUse(String blockType, int count, BufferedImage icon) {
}
}
@@ -0,0 +1,250 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.UnaryOperator;
/**
* The 3D view of one object. Drag to orbit, shift-drag or middle-drag to pan, scroll to zoom; arrow keys orbit and
* plus and minus zoom too. Frames are drawn on a background thread, one at a time, always for the latest camera.
*/
final class ObjectViewport extends JComponent {
private static final long serialVersionUID = -3175226517409913524L;
private static final double WHEEL_ZOOM_EXPONENT = 0.12D;
private static final double KEY_ZOOM_FACTOR = 1.1D;
private static final int KEY_ORBIT_PIXELS = 12;
private final ExecutorService renderThread = Executors.newSingleThreadExecutor(runnable -> {
Thread thread = new Thread(runnable, "Iris Object Viewer");
thread.setDaemon(true);
return thread;
});
private ObjectMesh mesh;
private ObjectCamera camera = ObjectCamera.START;
private BufferedImage frame;
private String message = "Choose an object";
private boolean rendering;
private boolean renderAgain;
private Point dragPoint;
private int coveredLeftWidth;
ObjectViewport() {
setOpaque(true);
setBackground(StudioTheme.EDITOR_BACKGROUND);
setFocusable(true);
installMouseControls();
installKeyControls();
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent event) {
requestFrame();
}
});
}
// The theme changed, and the last frame was drawn on the old theme's background.
@Override
public void updateUI() {
super.updateUI();
requestFrame();
}
void show(ObjectMesh nextMesh) {
mesh = nextMesh;
camera = ObjectCamera.START;
message = nextMesh.isEmpty() ? "No visible blocks" : null;
frame = null;
requestFrame();
repaint();
}
/**
* Swaps in another mesh of the same object, like one with its top layers cut away, keeping the camera.
*/
void replaceMesh(ObjectMesh nextMesh) {
mesh = nextMesh;
message = nextMesh.isEmpty() ? "No visible blocks" : null;
requestFrame();
repaint();
}
void bindKey(int keyCode, String name, Runnable action) {
getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(keyCode, 0), name);
getActionMap().put(name, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent event) {
action.run();
}
});
}
void showMessage(String text) {
mesh = null;
message = text;
frame = null;
repaint();
}
/**
* Keeps the object clear of a panel floating over the viewport's left side.
*/
void setCoveredLeftWidth(int width) {
if (width == coveredLeftWidth) {
return;
}
coveredLeftWidth = width;
requestFrame();
}
void resetCamera() {
moveCamera(current -> ObjectCamera.START);
}
void close() {
renderThread.shutdownNow();
}
private void moveCamera(UnaryOperator<ObjectCamera> move) {
camera = move.apply(camera);
requestFrame();
}
private void requestFrame() {
if (mesh == null || mesh.isEmpty() || getWidth() < 1 || getHeight() < 1) {
return;
}
if (rendering) {
renderAgain = true;
return;
}
rendering = true;
ObjectMesh frameMesh = mesh;
ObjectCamera frameCamera = camera;
int width = getWidth();
int height = getHeight();
int covered = coveredLeftWidth;
renderThread.execute(() -> {
BufferedImage image = ObjectRenderer.render(frameMesh, frameCamera, width, height, covered);
EventQueue.invokeLater(() -> finishFrame(frameMesh, image));
});
}
private void finishFrame(ObjectMesh frameMesh, BufferedImage image) {
rendering = false;
if (frameMesh == mesh) {
frame = image;
repaint();
}
if (renderAgain) {
renderAgain = false;
requestFrame();
}
}
@Override
protected void paintComponent(Graphics graphics) {
graphics.setColor(getBackground());
graphics.fillRect(0, 0, getWidth(), getHeight());
if (frame != null) {
graphics.drawImage(frame, 0, 0, null);
}
if (message != null) {
drawMessage((Graphics2D) graphics);
}
}
private void drawMessage(Graphics2D canvas) {
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
canvas.setFont(StudioTheme.font());
canvas.setColor(StudioTheme.MUTED_TEXT);
int textWidth = canvas.getFontMetrics().stringWidth(message);
canvas.drawString(message, (getWidth() - textWidth) / 2, getHeight() / 2);
}
private void installMouseControls() {
MouseAdapter mouse = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent event) {
dragPoint = event.getPoint();
requestFocusInWindow();
}
@Override
public void mouseReleased(MouseEvent event) {
dragPoint = null;
}
@Override
public void mouseDragged(MouseEvent event) {
if (dragPoint == null) {
return;
}
int draggedX = event.getX() - dragPoint.x;
int draggedY = event.getY() - dragPoint.y;
dragPoint = event.getPoint();
boolean panning = event.isShiftDown() || SwingUtilities.isMiddleMouseButton(event);
moveCamera(current -> panning
? current.pannedBy(2D * draggedX / Math.max(1, getWidth() - coveredLeftWidth), -2D * draggedY / getHeight())
: current.orbitedBy(draggedX, draggedY));
}
@Override
public void mouseWheelMoved(MouseWheelEvent event) {
moveCamera(current -> current.zoomedBy(Math.exp(-event.getPreciseWheelRotation() * WHEEL_ZOOM_EXPONENT)));
}
};
addMouseListener(mouse);
addMouseMotionListener(mouse);
addMouseWheelListener(mouse);
}
private void installKeyControls() {
bindKey(KeyEvent.VK_LEFT, "orbit-left", current -> current.orbitedBy(-KEY_ORBIT_PIXELS, 0));
bindKey(KeyEvent.VK_RIGHT, "orbit-right", current -> current.orbitedBy(KEY_ORBIT_PIXELS, 0));
bindKey(KeyEvent.VK_UP, "orbit-up", current -> current.orbitedBy(0, KEY_ORBIT_PIXELS));
bindKey(KeyEvent.VK_DOWN, "orbit-down", current -> current.orbitedBy(0, -KEY_ORBIT_PIXELS));
bindKey(KeyEvent.VK_EQUALS, "zoom-in", current -> current.zoomedBy(KEY_ZOOM_FACTOR));
bindKey(KeyEvent.VK_ADD, "zoom-in-keypad", current -> current.zoomedBy(KEY_ZOOM_FACTOR));
bindKey(KeyEvent.VK_MINUS, "zoom-out", current -> current.zoomedBy(1D / KEY_ZOOM_FACTOR));
bindKey(KeyEvent.VK_SUBTRACT, "zoom-out-keypad", current -> current.zoomedBy(1D / KEY_ZOOM_FACTOR));
}
private void bindKey(int keyCode, String name, UnaryOperator<ObjectCamera> move) {
bindKey(keyCode, name, () -> moveCamera(move));
}
}
@@ -0,0 +1,263 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonReader;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.engine.object.IrisBlockData;
import com.volmit.iris.engine.object.IrisRegion;
import com.volmit.iris.util.collection.KList;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
record PaletteCatalog(List<RegionEntry> regions, List<String> biomesOutsideRegions, Map<String, BiomeEntry> biomes,
List<SnippetEntry> snippets) {
private static final int SUMMARIZED_BLOCK_COUNT = 3;
static PaletteCatalog load(Engine engine) {
IrisData data = engine.getData();
Map<String, BiomeEntry> biomes = loadBiomes(data);
List<RegionEntry> regions = new ArrayList<>();
Set<String> biomesInRegions = new HashSet<>();
for (String regionKey : regionKeysInUse(engine)) {
IrisRegion region = data.getRegionLoader().load(regionKey);
if (region == null) {
continue;
}
RegionEntry entry = describeRegion(regionKey, region, biomes);
regions.add(entry);
entry.groups().forEach(group -> group.biomeKeys().forEach(key -> collectWithChildren(key, biomes, biomesInRegions)));
}
List<String> outside = biomes.keySet().stream().filter(key -> !biomesInRegions.contains(key)).toList();
return new PaletteCatalog(List.copyOf(regions), outside, Map.copyOf(biomes), loadSnippets(data, biomes));
}
// Snippet files no biome refers to are listed too, so a new snippet shows up before it is put to use.
private static List<SnippetEntry> loadSnippets(IrisData data, Map<String, BiomeEntry> biomes) {
Map<String, SnippetEntry> snippets = new LinkedHashMap<>();
for (BiomeEntry biome : biomes.values()) {
for (PaletteSummary summary : biome.palettes()) {
if (summary.snippet() == null) {
continue;
}
BiomePalette.SnippetFolder folder = summary.palette().kind().snippetFolder();
snippets.computeIfAbsent(folder.folder() + "/" + summary.snippet(),
key -> new SnippetEntry(folder, summary.snippet(), summary.blocks(), new ArrayList<>()))
.uses().add(summary.palette());
}
}
for (BiomePalette.SnippetFolder folder : BiomePalette.SnippetFolder.values()) {
for (String reference : data.getPossibleSnippets(folder.folder())) {
String path = folder.pathOf(reference);
snippets.putIfAbsent(folder.folder() + "/" + path, new SnippetEntry(folder, path, null, List.of()));
}
}
return snippets.values().stream()
.map(snippet -> new SnippetEntry(snippet.folder(), snippet.path(), snippet.blocks(), List.copyOf(snippet.uses())))
.sorted(Comparator.comparing(SnippetEntry::folder).thenComparing(SnippetEntry::path))
.toList();
}
// A studio focus region is generated even when the dimension's region list doesn't include it.
private static Set<String> regionKeysInUse(Engine engine) {
Set<String> keys = new LinkedHashSet<>();
if (engine.getDimension().hasFocusRegion()) {
keys.add(engine.getDimension().getFocusRegion());
}
keys.addAll(engine.getDimension().getRegions());
return keys;
}
BiomeEntry biome(String key) {
return key == null ? null : biomes.get(key);
}
RegionEntry region(String key) {
return regions.stream().filter(region -> region.key().equals(key)).findFirst().orElse(null);
}
static String summarizeBlocks(KList<IrisBlockData> blocks) {
List<String> names = blocks.stream()
.map(block -> block.getBlock().replace("minecraft:", "").toLowerCase(Locale.ROOT))
.distinct()
.toList();
if (names.size() <= SUMMARIZED_BLOCK_COUNT) {
return String.join(", ", names);
}
return String.join(", ", names.subList(0, SUMMARIZED_BLOCK_COUNT))
+ " +" + (names.size() - SUMMARIZED_BLOCK_COUNT) + " more";
}
private static Map<String, BiomeEntry> loadBiomes(IrisData data) {
String[] keys = data.getBiomeLoader().getPossibleKeys();
Map<String, IrisBiome> loaded = new LinkedHashMap<>();
if (keys == null) {
return Map.of();
}
Arrays.sort(keys);
for (String key : keys) {
IrisBiome biome = data.getBiomeLoader().load(key);
if (biome != null) {
loaded.put(key, biome);
}
}
Map<String, List<String>> parentsByChild = parentsByChild(loaded);
Map<String, BiomeEntry> biomes = new LinkedHashMap<>();
loaded.forEach((key, biome) -> biomes.put(key, new BiomeEntry(
key,
biome.getName(),
summarizePalettes(key, biome),
biome.getChildren().stream().filter(loaded::containsKey).distinct().toList(),
parentsByChild.getOrDefault(key, List.of())
)));
return biomes;
}
private static Map<String, List<String>> parentsByChild(Map<String, IrisBiome> biomes) {
Map<String, List<String>> parents = new LinkedHashMap<>();
biomes.forEach((parentKey, parent) -> parent.getChildren().stream().distinct().forEach(childKey ->
parents.computeIfAbsent(childKey, key -> new ArrayList<>()).add(parentKey)));
return parents;
}
static List<PaletteSummary> summarizePalettes(String key, IrisBiome biome) {
JsonObject biomeSource = readSource(biome.getLoadFile());
return BiomePalette.allOf(key, biome).stream()
.map(palette -> new PaletteSummary(palette, snippetName(biomeSource, palette),
summarizeBlocks(palette.blocksIn(biome))))
.toList();
}
// Snippets are copied into the biome when it loads, so only its file still tells which palettes came from one.
private static String snippetName(JsonObject biomeSource, BiomePalette palette) {
JsonElement element = biomeSource == null ? null : biomeSource.get(palette.kind().jsonKey());
if (element instanceof JsonArray list) {
element = palette.index() < list.size() ? list.get(palette.index()) : null;
}
if (element == null || !element.isJsonPrimitive()) {
return null;
}
return palette.kind().snippetFolder().pathOf(element.getAsString());
}
private static JsonObject readSource(File file) {
if (file == null || !file.isFile()) {
return null;
}
try (JsonReader reader = new JsonReader(new FileReader(file))) {
reader.setLenient(true);
JsonElement source = JsonParser.parseReader(reader);
return source.isJsonObject() ? source.getAsJsonObject() : null;
} catch (IOException | RuntimeException e) {
return null;
}
}
private static RegionEntry describeRegion(String key, IrisRegion region, Map<String, BiomeEntry> biomes) {
List<BiomeGroup> groups = new ArrayList<>();
addGroup(groups, "Land", region.getLandBiomes(), biomes);
addGroup(groups, "Shore", region.getShoreBiomes(), biomes);
addGroup(groups, "Sea", region.getSeaBiomes(), biomes);
addGroup(groups, "Cave", region.getCaveBiomes(), biomes);
return new RegionEntry(key, region.getName(), List.copyOf(groups));
}
private static void addGroup(List<BiomeGroup> groups, String label, List<String> keys, Map<String, BiomeEntry> biomes) {
List<String> known = keys.stream().filter(biomes::containsKey).distinct().toList();
if (!known.isEmpty()) {
groups.add(new BiomeGroup(label, known));
}
}
private static void collectWithChildren(String key, Map<String, BiomeEntry> biomes, Set<String> collected) {
BiomeEntry biome = biomes.get(key);
if (biome == null || !collected.add(key)) {
return;
}
biome.childKeys().forEach(child -> collectWithChildren(child, biomes, collected));
}
record RegionEntry(String key, String name, List<BiomeGroup> groups) {
List<String> allBiomeKeys() {
return groups.stream().flatMap(group -> group.biomeKeys().stream()).distinct().toList();
}
}
record BiomeGroup(String label, List<String> biomeKeys) {
}
/**
* @param parentKeys the biomes that list this one as a child, which it can appear inside of
*/
record BiomeEntry(String key, String name, List<PaletteSummary> palettes, List<String> childKeys,
List<String> parentKeys) {
boolean matches(String filter) {
if (key.toLowerCase().contains(filter) || name.toLowerCase().contains(filter)) {
return true;
}
return palettes.stream().anyMatch(summary -> summary.text().toLowerCase().contains(filter));
}
BiomePalette firstPalette() {
return palettes.isEmpty() ? null : palettes.get(0).palette();
}
}
/**
* A snippet file palettes can come from.
*
* @param blocks a summary of its blocks, or null when no biome uses it
* @param uses the palettes made from it, in biome order
*/
record SnippetEntry(BiomePalette.SnippetFolder folder, String path, String blocks, List<BiomePalette> uses) {
boolean matches(String filter) {
return path.toLowerCase(Locale.ROOT).contains(filter)
|| (blocks != null && blocks.toLowerCase(Locale.ROOT).contains(filter));
}
}
/**
* @param snippet the path of the snippet file the palette comes from, or null when the biome defines it itself
*/
record PaletteSummary(BiomePalette palette, String snippet, String blocks) {
String text() {
return snippet == null
? palette.label() + " · " + blocks
: palette.label() + " · " + snippet + " snippet · " + blocks;
}
}
}
@@ -16,13 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.engine.mantle;
package com.volmit.iris.core.gui;
import com.volmit.iris.util.mantle.MantleFlag;
import lombok.Data;
import com.volmit.iris.engine.framework.Engine;
@Data
public abstract class IrisMantleComponent implements MantleComponent {
private final EngineMantle engineMantle;
private final MantleFlag flag;
import java.util.UUID;
public record PaletteExplorerStart(Engine engine, UUID worldId, UUID playerId, String biomeKey, double centerX, double centerZ) {
}
@@ -0,0 +1,500 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JToggleButton;
import javax.swing.SwingConstants;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.RenderingHints;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
final class PaletteInspectorPanel extends JPanel {
private static final long serialVersionUID = 4473102938518712001L;
private static final int PANEL_WIDTH = 310;
private static final Color HISTOGRAM_BAR = new Color(0x9D, 0xA0, 0xA8, 150);
private static final Color BAND_DIVIDER = new Color(255, 255, 255, 70);
private final Listener listener;
private final JLabel titleLabel = new JLabel();
private final JLabel detailLabel = new JLabel();
private final JLabel depthLabel = new JLabel("", SwingConstants.CENTER);
private final JButton shallowerButton = new JButton("‹");
private final JButton deeperButton = new JButton("›");
private final JPanel depthRow = new JPanel(new BorderLayout(StudioTheme.scale(6), 0));
private final StatisticsView statisticsView = new StatisticsView();
private PaletteLayerScale.View selectedView = PaletteLayerScale.View.BLOCKS;
private int depth;
private int deepestDepth;
PaletteInspectorPanel(Listener listener) {
super(new BorderLayout());
this.listener = listener;
setPreferredSize(StudioTheme.scaledSize(PANEL_WIDTH, 0));
setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, StudioTheme.SEPARATOR));
add(buildControls(), BorderLayout.NORTH);
add(StudioToolbar.scroll(statisticsView), BorderLayout.CENTER);
setVisible(false);
}
void showPreview(PaletteLayerPreview preview) {
titleLabel.setText(preview.biomeKey() + " · " + preview.palette().lowercaseLabel());
detailLabel.setText(placementText(preview));
depth = preview.depth();
deepestDepth = Math.max(0, preview.maxThickness() - 1);
refreshDepthControls();
statisticsView.show(preview);
setVisible(true);
}
void showTextureStatus(String status) {
statisticsView.showTextureStatus(status);
}
void hideInspector() {
setVisible(false);
}
PaletteLayerScale.View selectedView() {
return selectedView;
}
private JPanel buildControls() {
JPanel controls = new JPanel(new GridLayout(0, 1, 0, StudioTheme.scale(6)));
controls.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 1, 0, StudioTheme.SEPARATOR),
StudioTheme.emptyBorder(12, 14, 12, 14)
));
titleLabel.setFont(StudioTheme.titleFont());
detailLabel.setForeground(StudioTheme.SECONDARY_TEXT);
detailLabel.setFont(StudioTheme.smallFont());
controls.add(titleLabel);
controls.add(detailLabel);
controls.add(sectionLabel("Show"));
controls.add(buildViewButtons());
controls.add(sectionLabel("Depth"));
controls.add(buildDepthRow());
return controls;
}
private JPanel buildViewButtons() {
JPanel row = new JPanel(new GridLayout(1, 0, StudioTheme.scale(4), 0));
row.setOpaque(false);
ButtonGroup group = new ButtonGroup();
for (PaletteLayerScale.View view : PaletteLayerScale.View.values()) {
JToggleButton button = new JToggleButton(view.toString());
button.setFocusable(false);
button.setSelected(view == selectedView);
button.addActionListener(event -> {
selectedView = view;
listener.onViewChanged(view);
});
group.add(button);
row.add(button);
}
return row;
}
private JPanel buildDepthRow() {
depthRow.setOpaque(false);
shallowerButton.setToolTipText("Show the block one higher up");
deeperButton.setToolTipText("Show the block one further down");
shallowerButton.addActionListener(event -> changeDepth(-1));
deeperButton.addActionListener(event -> changeDepth(1));
depthRow.add(shallowerButton, BorderLayout.WEST);
depthRow.add(depthLabel, BorderLayout.CENTER);
depthRow.add(deeperButton, BorderLayout.EAST);
return depthRow;
}
private void changeDepth(int step) {
int nextDepth = Math.clamp(depth + step, 0, deepestDepth);
if (nextDepth == depth) {
return;
}
depth = nextDepth;
refreshDepthControls();
listener.onDepthChanged(depth);
}
private void refreshDepthControls() {
depthLabel.setText(depthText(depth, deepestDepth));
shallowerButton.setEnabled(depth > 0);
deeperButton.setEnabled(depth < deepestDepth);
}
private static JLabel sectionLabel(String text) {
JLabel label = new JLabel(text.toUpperCase(Locale.ROOT));
label.setForeground(StudioTheme.MUTED_TEXT);
label.setFont(StudioTheme.sectionFont());
return label;
}
// Stacked layers divide their coordinates by the zoom twice before sampling; wall and slab blocks only once.
private static String placementText(PaletteLayerPreview preview) {
if (!preview.palette().kind().isStacked()) {
return "Single blocks · zoom " + formatZoom(preview.zoom());
}
return thicknessText(preview) + " · zoom " + formatZoom(preview.zoom())
+ " (×" + formatZoom(preview.zoom() * preview.zoom()) + " effective)";
}
private static String thicknessText(PaletteLayerPreview preview) {
if (preview.minThickness() == preview.maxThickness()) {
return preview.maxThickness() + (preview.maxThickness() == 1 ? " block thick" : " blocks thick");
}
return preview.minThickness() + "–" + preview.maxThickness() + " blocks thick";
}
private static String depthText(int depth, int deepestDepth) {
if (deepestDepth == 0) {
return "Top block only";
}
return depth == 0 ? "Top block" : depth + (depth == 1 ? " block down" : " blocks down");
}
private static String formatZoom(double zoom) {
return zoom == Math.rint(zoom) ? Long.toString((long) zoom) : String.format(Locale.ROOT, "%.2f", zoom);
}
private static String percent(double share) {
if (share > 0D && share < 0.001D) {
return "<0.1%";
}
return String.format(Locale.ROOT, "%.1f%%", share * 100D);
}
interface Listener {
void onViewChanged(PaletteLayerScale.View view);
void onDepthChanged(int depth);
}
private static final class StatisticsView extends JComponent {
private static final long serialVersionUID = -2318840125766150871L;
private static final int MARGIN = 14;
private static final int HISTOGRAM_HEIGHT = 84;
private static final int BAND_HEIGHT = 12;
private static final int SECTION_GAP = 18;
private static final int TITLE_GAP = 6;
private static final int NOTE_GAP = 4;
private static final String WIDEST_WEIGHT = "100.0%";
private static final String WIDEST_ACTUAL = "▲ 100.0%";
private PaletteLayerPreview preview;
private String textureStatus;
private StatisticsView() {
setOpaque(true);
setBackground(StudioTheme.PANEL_BACKGROUND);
}
private void show(PaletteLayerPreview nextPreview) {
preview = nextPreview;
revalidate();
repaint();
}
private void showTextureStatus(String status) {
textureStatus = status;
revalidate();
repaint();
}
@Override
public Dimension getPreferredSize() {
int preferredWidth = StudioTheme.scale(PANEL_WIDTH - 20);
if (preview == null) {
return new Dimension(preferredWidth, 0);
}
int margin = StudioTheme.scale(MARGIN);
int width = Math.max(getWidth(), preferredWidth) - margin * 2;
int height = margin + valueSpreadHeight() + StudioTheme.scale(SECTION_GAP) + blockTableHeight()
+ StudioTheme.scale(SECTION_GAP) + notesHeight(width) + margin;
return new Dimension(preferredWidth, height);
}
@Override
protected void paintComponent(Graphics graphics) {
Graphics2D canvas = (Graphics2D) graphics.create();
try {
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
canvas.setColor(StudioTheme.PANEL_BACKGROUND);
canvas.fillRect(0, 0, getWidth(), getHeight());
if (preview == null) {
return;
}
int margin = StudioTheme.scale(MARGIN);
int width = getWidth() - margin * 2;
int top = margin;
drawValueSpread(canvas, margin, top, width);
top += valueSpreadHeight() + StudioTheme.scale(SECTION_GAP);
drawBlockTable(canvas, margin, top, width);
top += blockTableHeight() + StudioTheme.scale(SECTION_GAP);
drawNotes(canvas, margin, top, width);
} finally {
canvas.dispose();
}
}
private int valueSpreadHeight() {
return sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP) + StudioTheme.scale(HISTOGRAM_HEIGHT)
+ StudioTheme.scale(2) + StudioTheme.scale(BAND_HEIGHT) + StudioTheme.scale(3) + smallMetrics().getHeight();
}
private int blockTableHeight() {
return sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP) + rowHeight() * preview.entries().size();
}
private int notesHeight(int width) {
FontMetrics metrics = smallMetrics();
int height = 0;
for (String note : notes()) {
height += wrap(note, metrics, width).size() * metrics.getHeight() + StudioTheme.scale(NOTE_GAP);
}
return height;
}
private int rowHeight() {
return textMetrics().getHeight() + StudioTheme.scale(4);
}
private FontMetrics textMetrics() {
return getFontMetrics(StudioTheme.font());
}
private FontMetrics smallMetrics() {
return getFontMetrics(StudioTheme.smallFont());
}
private FontMetrics sectionMetrics() {
return getFontMetrics(StudioTheme.sectionFont());
}
private void drawValueSpread(Graphics2D canvas, int x, int top, int width) {
drawSectionTitle(canvas, "Noise values this layer produces", x, top + sectionMetrics().getAscent());
int histogramTop = top + sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP);
int histogramHeight = StudioTheme.scale(HISTOGRAM_HEIGHT);
int bandHeight = StudioTheme.scale(BAND_HEIGHT);
long[] histogram = preview.histogram();
long peak = 1L;
for (long count : histogram) {
peak = Math.max(peak, count);
}
canvas.setColor(HISTOGRAM_BAR);
for (int bin = 0; bin < histogram.length; bin++) {
int barLeft = x + (bin * width) / histogram.length;
int barRight = x + ((bin + 1) * width) / histogram.length;
int barHeight = (int) Math.round(histogramHeight * (histogram[bin] / (double) peak));
canvas.fillRect(barLeft, histogramTop + histogramHeight - barHeight, Math.max(1, barRight - barLeft - 1), barHeight);
}
int bandTop = histogramTop + histogramHeight + StudioTheme.scale(2);
for (PaletteLayerPreview.Entry entry : preview.entries()) {
if (Double.isNaN(entry.lowestNoise())) {
continue;
}
int bandLeft = x + (int) Math.round(entry.lowestNoise() * width);
int bandRight = x + (int) Math.round(entry.highestNoise() * width);
canvas.setColor(new Color(entry.color()));
canvas.fillRect(bandLeft, bandTop, Math.max(1, bandRight - bandLeft), bandHeight);
if (entry.highestNoise() < 1D) {
canvas.setColor(BAND_DIVIDER);
canvas.drawLine(bandRight, histogramTop, bandRight, bandTop + bandHeight - 1);
}
}
canvas.setFont(StudioTheme.smallFont());
canvas.setColor(StudioTheme.SECONDARY_TEXT);
FontMetrics metrics = canvas.getFontMetrics();
int labelY = bandTop + bandHeight + StudioTheme.scale(3) + metrics.getAscent();
canvas.drawString("0", x, labelY);
canvas.drawString("0.5", x + (width - metrics.stringWidth("0.5")) / 2, labelY);
canvas.drawString("1", x + width - metrics.stringWidth("1"), labelY);
}
private void drawBlockTable(Graphics2D canvas, int x, int top, int width) {
int columnGap = StudioTheme.scale(10);
int actualRight = x + width;
int weightRight = actualRight - textMetrics().stringWidth(WIDEST_ACTUAL) - columnGap;
int nameRight = weightRight - textMetrics().stringWidth(WIDEST_WEIGHT) - columnGap;
int titleBaseline = top + sectionMetrics().getAscent();
drawSectionTitle(canvas, "Blocks", x, titleBaseline);
drawRightAligned(canvas, "weight", weightRight, titleBaseline);
drawRightAligned(canvas, "actual", actualRight, titleBaseline);
int rowHeight = rowHeight();
FontMetrics text = textMetrics();
int swatchSize = text.getAscent() - text.getDescent();
int rowTop = top + sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP);
for (PaletteLayerPreview.Entry entry : preview.entries()) {
int baseline = rowTop + (rowHeight + text.getAscent() - text.getDescent()) / 2;
drawSwatch(canvas, entry, x, rowTop + (rowHeight - swatchSize) / 2, swatchSize);
canvas.setFont(StudioTheme.font());
canvas.setColor(entry.found() ? StudioTheme.TEXT : StudioTheme.ERROR);
String name = entry.found() ? shortBlockName(entry.block()) : shortBlockName(entry.block()) + " (not found)";
int nameLeft = x + swatchSize + StudioTheme.scale(7);
canvas.drawString(fit(canvas, name, nameRight - nameLeft), nameLeft, baseline);
canvas.setColor(StudioTheme.SECONDARY_TEXT);
drawRightAligned(canvas, percent(entry.weightShare()), weightRight, baseline);
canvas.setColor(actualColor(entry));
drawRightAligned(canvas, actualText(entry), actualRight, baseline);
rowTop += rowHeight;
}
}
private static void drawSwatch(Graphics2D canvas, PaletteLayerPreview.Entry entry, int x, int y, int size) {
if (entry.texture() != null) {
canvas.drawImage(entry.texture().toImage(), x, y, size, size, null);
return;
}
canvas.setColor(new Color(entry.color()));
int arc = StudioTheme.scale(3);
canvas.fillRoundRect(x, y, size, size, arc, arc);
}
private void drawNotes(Graphics2D canvas, int x, int top, int width) {
canvas.setFont(StudioTheme.smallFont());
canvas.setColor(StudioTheme.SECONDARY_TEXT);
FontMetrics metrics = canvas.getFontMetrics();
int lineY = top + metrics.getAscent();
for (String note : notes()) {
for (String line : wrap(note, metrics, width)) {
canvas.drawString(line, x, lineY);
lineY += metrics.getHeight();
}
lineY += StudioTheme.scale(NOTE_GAP);
}
}
private List<String> notes() {
List<String> notes = new ArrayList<>();
if (textureStatus != null) {
notes.add(textureStatus);
}
notes.add("▼ gets under half its weight, ▲ gets over double.");
notes.add(String.format(Locale.ROOT, "Measured on a %d × %d grid spread over %,.0f blocks around 0, 0.",
PaletteLayerPreview.STATISTICS_GRID_SIZE, PaletteLayerPreview.STATISTICS_GRID_SIZE,
PaletteLayerPreview.statisticsAreaBlocks()));
if (preview.absentShare() > 0D) {
notes.add(absentText(preview) + " That ground is dark on the map and left out of the shares.");
}
if (preview.outOfRangeShare() > 0D) {
notes.add(percent(preview.outOfRangeShare()) + " of the noise values fall outside 0 to 1."
+ " Those spots get the first block.");
}
if (preview.simplification() != null) {
notes.add(preview.simplification());
}
if (preview.hasSlopeCondition()) {
notes.add("This " + paletteNoun(preview) + " has a slope condition, which the preview ignores.");
}
return notes;
}
private static String absentText(PaletteLayerPreview preview) {
if (isDecorator(preview)) {
return "The decorator leaves " + percent(preview.absentShare()) + " of the ground bare.";
}
return "The layer doesn't reach this depth on " + percent(preview.absentShare()) + " of the ground.";
}
private static String paletteNoun(PaletteLayerPreview preview) {
return isDecorator(preview) ? "decorator" : "layer";
}
private static boolean isDecorator(PaletteLayerPreview preview) {
return preview.palette().kind() == BiomePalette.Kind.DECORATOR;
}
private static String actualText(PaletteLayerPreview.Entry entry) {
if (entry.isFarBelowWeight()) {
return "▼ " + percent(entry.actualShare());
}
if (entry.isFarAboveWeight()) {
return "▲ " + percent(entry.actualShare());
}
return percent(entry.actualShare());
}
private static Color actualColor(PaletteLayerPreview.Entry entry) {
return entry.isFarBelowWeight() || entry.isFarAboveWeight() ? StudioTheme.WARNING : StudioTheme.TEXT;
}
private static void drawSectionTitle(Graphics2D canvas, String title, int x, int y) {
canvas.setFont(StudioTheme.sectionFont());
canvas.setColor(StudioTheme.MUTED_TEXT);
canvas.drawString(title.toUpperCase(Locale.ROOT), x, y);
}
private static void drawRightAligned(Graphics2D canvas, String text, int right, int y) {
canvas.drawString(text, right - canvas.getFontMetrics().stringWidth(text), y);
}
private static String shortBlockName(String block) {
return block.startsWith("minecraft:") ? block.substring("minecraft:".length()) : block;
}
private static String fit(Graphics2D canvas, String text, int maxWidth) {
FontMetrics metrics = canvas.getFontMetrics();
if (metrics.stringWidth(text) <= maxWidth) {
return text;
}
String shortened = text;
while (shortened.length() > 1 && metrics.stringWidth(shortened + "…") > maxWidth) {
shortened = shortened.substring(0, shortened.length() - 1);
}
return shortened + "…";
}
private static List<String> wrap(String text, FontMetrics metrics, int maxWidth) {
List<String> lines = new ArrayList<>();
StringBuilder line = new StringBuilder();
for (String word : text.split(" ")) {
String candidate = line.isEmpty() ? word : line + " " + word;
if (metrics.stringWidth(candidate) > maxWidth && !line.isEmpty()) {
lines.add(line.toString());
line = new StringBuilder(word);
} else {
line = new StringBuilder(candidate);
}
}
if (!line.isEmpty()) {
lines.add(line.toString());
}
return lines;
}
}
}
@@ -0,0 +1,457 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
import com.volmit.iris.core.loader.IrisData;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.engine.object.IrisBiomePaletteLayer;
import com.volmit.iris.engine.object.IrisBlockData;
import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.math.RNG;
import org.bukkit.block.data.BlockData;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
final class PaletteLayerPreview {
static final int HISTOGRAM_BINS = 50;
static final int STATISTICS_GRID_SIZE = 64;
static final double STATISTICS_SPACING_BLOCKS = 37D;
private static final int MISSING_BLOCK_COLOR = 0x5A5F6B;
private static final double FAR_FROM_WEIGHT_FACTOR = 2D;
private static final int BLOCK_CACHE_SIZE = 1 << 16;
private static final int NO_OVERHANG = -1;
private final BiomePalette palette;
private final KList<IrisBlockData> blocks;
private final BiomePalettePlacement placement;
private final PaletteLayerPreview ground;
private final boolean[] showsGroundByEntry;
private final int depth;
private final IrisData data;
private final int[] entryBySlot;
private final BiomeTintField biomeTints;
private final BlockTexture[] textureByEntry;
private final BlockTexture[] upperHalfByEntry;
private final boolean hasTallPlants;
private final BlockTexture[] legendTextureByEntry;
private final int[] legendColorByEntry;
private final List<Entry> entries;
private final SampledBlock[] blockCache = new SampledBlock[BLOCK_CACHE_SIZE];
private final long[] histogram;
private final double absentShare;
private final double outOfRangeShare;
private PaletteLayerPreview(BiomePalette palette, BiomePalettePlacement placement, PaletteLayerPreview ground, int depth,
IrisData data, BlockTextures textures, BiomeTintField biomeTints) {
this.palette = palette;
this.blocks = placement.blocks();
this.placement = placement;
this.ground = ground;
this.depth = depth;
this.data = data;
this.entryBySlot = IrisBiomePaletteLayer.paletteEntryPerSlot(blocks, data).stream().mapToInt(Integer::intValue).toArray();
if (entryBySlot.length == 0) {
throw new IllegalStateException("No block of " + palette.biomeKey() + " " + palette.lowercaseLabel() + " could be found");
}
this.biomeTints = biomeTints;
this.showsGroundByEntry = resolveGroundShowingEntries();
this.textureByEntry = resolveEntryTextures(textures);
this.upperHalfByEntry = resolveUpperHalves(textures);
this.hasTallPlants = Arrays.stream(upperHalfByEntry).anyMatch(Objects::nonNull);
this.legendTextureByEntry = tintForLegend(legendTextures());
this.legendColorByEntry = resolveLegendColors();
Statistics statistics = measure();
this.histogram = statistics.histogram();
this.absentShare = statistics.absentShare();
this.outOfRangeShare = statistics.outOfRangeShare();
this.entries = describeEntries(statistics.sharesByEntry());
}
static PaletteLayerPreview load(Engine engine, BiomePalette palette, int depth, BlockTextures textures) {
if (engine == null || engine.isClosed()) {
throw new IllegalStateException("Open a studio world to preview pack palettes");
}
IrisData data = engine.getData();
IrisBiome biome = data.getBiomeLoader().load(palette.biomeKey());
if (biome == null || palette.blocksIn(biome) == null) {
throw new IllegalStateException("Palette removed during hotload: " + palette.biomeKey() + " " + palette.lowercaseLabel());
}
BiomePalettePlacement placement = BiomePalettePlacement.of(engine, biome, palette);
BiomeTintField biomeTints = textures == null
? null
: new BiomeTintField(biome, new RNG(engine.getSeedManager().getBiome()), textures.biomeColors());
PaletteLayerPreview ground = palette.kind() == BiomePalette.Kind.DECORATOR
? groundUnder(engine, palette.biomeKey(), biome, data, textures, biomeTints)
: null;
return new PaletteLayerPreview(palette, placement, ground, depth, data, textures, biomeTints);
}
// Decorations stand on the biome's top surface layer, so that is what shows around and between them.
private static PaletteLayerPreview groundUnder(Engine engine, String biomeKey, IrisBiome biome, IrisData data,
BlockTextures textures, BiomeTintField biomeTints) {
BiomePalette topLayer = new BiomePalette(biomeKey, BiomePalette.Kind.LAYER, 0);
KList<IrisBlockData> blocks = topLayer.blocksIn(biome);
if (blocks == null || blocks.isEmpty()) {
return null;
}
return new PaletteLayerPreview(topLayer, BiomePalettePlacement.of(engine, biome, topLayer), null, 0, data, textures,
biomeTints);
}
// The world picks one block per whole coordinate, and zoomed in, many pixels fall on the same block.
double sample(double x, double z) {
int blockX = (int) Math.floor(x);
int blockZ = (int) Math.floor(z);
int cacheSlot = blockCacheSlot(blockX, blockZ);
SampledBlock cached = blockCache[cacheSlot];
if (cached != null && cached.x() == blockX && cached.z() == blockZ) {
return cached.noise();
}
double noise = sampleBlock(blockX, blockZ);
blockCache[cacheSlot] = new SampledBlock(blockX, blockZ, noise);
return noise;
}
private double sampleBlock(int blockX, int blockZ) {
if (placement.thickness(blockX, blockZ) <= depth) {
return Double.NaN;
}
return placement.noise(blockX, blockZ, depth);
}
private static int blockCacheSlot(int blockX, int blockZ) {
long hash = blockX * 0x9E3779B97F4A7C15L ^ blockZ * 0xC2B2AE3D27D4EB4FL;
return (int) ((hash ^ (hash >>> 29)) & (BLOCK_CACHE_SIZE - 1));
}
int colorForNoise(double noise) {
return legendColorByEntry[entryIndexForNoise(noise)];
}
int blockColorAt(double noise, double worldX, double worldZ, boolean showTexturePixels) {
int overhang = showTexturePixels ? overhangColorAt(worldX, worldZ) : NO_OVERHANG;
if (overhang != NO_OVERHANG) {
return overhang;
}
int entry = entryIndexForNoise(noise);
if (showsGroundByEntry[entry]) {
return bareColorAt(worldX, worldZ, showTexturePixels);
}
BlockTexture texture = textureByEntry[entry];
if (texture == null) {
return legendColorByEntry[entry];
}
if (ground != null && showTexturePixels && !texture.isOpaqueAt(worldX, worldZ)) {
return groundColorAt(worldX, worldZ, true);
}
int untintedColor = showTexturePixels ? texture.texelAt(worldX, worldZ) : texture.averageColor();
return tinted(texture, untintedColor, worldX, worldZ);
}
/**
* The colour where the palette places nothing: the ground under a decorator, or plain dark otherwise.
*/
int bareColorAt(double worldX, double worldZ, boolean showTexturePixels) {
if (ground == null) {
return PaletteLayerScale.ABSENT_COLOR;
}
int overhang = showTexturePixels ? overhangColorAt(worldX, worldZ) : NO_OVERHANG;
return overhang != NO_OVERHANG ? overhang : groundColorAt(worldX, worldZ, showTexturePixels);
}
// A two block tall plant stands up on the map: its top half covers the block above it, which is one block north.
private int overhangColorAt(double worldX, double worldZ) {
if (!hasTallPlants) {
return NO_OVERHANG;
}
double plantZ = worldZ + 1D;
double noise = sample(worldX, plantZ);
if (Double.isNaN(noise)) {
return NO_OVERHANG;
}
BlockTexture upperHalf = upperHalfByEntry[entryIndexForNoise(noise)];
if (upperHalf == null || !upperHalf.isOpaqueAt(worldX, worldZ)) {
return NO_OVERHANG;
}
return tinted(upperHalf, upperHalf.texelAt(worldX, worldZ), worldX, plantZ);
}
private int tinted(BlockTexture texture, int untintedColor, double worldX, double worldZ) {
if (texture.biomeTint() == null) {
return untintedColor;
}
int tint = biomeTints.colorAt(texture.biomeTint(), (int) Math.floor(worldX), (int) Math.floor(worldZ));
return BlockTexture.multiply(untintedColor, tint);
}
private int groundColorAt(double worldX, double worldZ, boolean showTexturePixels) {
double noise = ground.sample(worldX, worldZ);
return Double.isNaN(noise) ? PaletteLayerScale.ABSENT_COLOR : ground.blockColorAt(noise, worldX, worldZ, showTexturePixels);
}
// A decorator places air to leave gaps, which looks the same as placing nothing.
private boolean[] resolveGroundShowingEntries() {
boolean[] showsGround = new boolean[blocks.size()];
for (int entry = 0; entry < blocks.size(); entry++) {
BlockData block = blocks.get(entry).getBlockData(data);
showsGround[entry] = ground != null && block != null && block.getMaterial().isAir();
}
return showsGround;
}
Entry entryForNoise(double noise) {
return entries.get(entryIndexForNoise(noise));
}
String biomeKey() {
return palette.biomeKey();
}
BiomePalette palette() {
return palette;
}
String simplification() {
return placement.simplification();
}
int depth() {
return depth;
}
int minThickness() {
return placement.minThickness();
}
int maxThickness() {
return placement.maxThickness();
}
double zoom() {
return placement.zoom();
}
boolean hasSlopeCondition() {
return placement.hasSlopeCondition();
}
List<Entry> entries() {
return entries;
}
long[] histogram() {
return histogram.clone();
}
double absentShare() {
return absentShare;
}
double outOfRangeShare() {
return outOfRangeShare;
}
static double statisticsAreaBlocks() {
return STATISTICS_GRID_SIZE * STATISTICS_SPACING_BLOCKS;
}
private int entryIndexForNoise(double noise) {
return entryBySlot[IrisBiomePaletteLayer.slotForNoise(noise, entryBySlot.length)];
}
private BlockTexture[] resolveEntryTextures(BlockTextures textures) {
BlockTexture[] resolved = new BlockTexture[blocks.size()];
if (textures == null) {
return resolved;
}
for (int entry = 0; entry < blocks.size(); entry++) {
BlockData block = blocks.get(entry).getBlockData(data);
resolved[entry] = block == null ? null : textureOf(block, textures).orElse(null);
}
return resolved;
}
private BlockTexture[] resolveUpperHalves(BlockTextures textures) {
BlockTexture[] resolved = new BlockTexture[blocks.size()];
if (textures == null || palette.kind() != BiomePalette.Kind.DECORATOR) {
return resolved;
}
for (int entry = 0; entry < blocks.size(); entry++) {
BlockData block = blocks.get(entry).getBlockData(data);
resolved[entry] = block == null ? null : textures.upperHalfIcon(block).orElse(null);
}
return resolved;
}
// Tall plants are known by their top half, the way Minecraft draws them in the inventory.
private BlockTexture[] legendTextures() {
BlockTexture[] legend = textureByEntry.clone();
for (int entry = 0; entry < legend.length; entry++) {
if (upperHalfByEntry[entry] != null) {
legend[entry] = upperHalfByEntry[entry];
}
}
return legend;
}
private Optional<BlockTexture> textureOf(BlockData block, BlockTextures textures) {
return palette.kind() == BiomePalette.Kind.DECORATOR ? textures.iconTexture(block) : textures.topTexture(block);
}
// The legend shows tinted blocks as they look at the world origin, where the statistics are sampled too.
private BlockTexture[] tintForLegend(BlockTexture[] textures) {
BlockTexture[] tinted = new BlockTexture[textures.length];
for (int entry = 0; entry < textures.length; entry++) {
BlockTexture texture = textures[entry];
tinted[entry] = texture == null || texture.biomeTint() == null
? texture
: texture.tinted(biomeTints.colorAt(texture.biomeTint(), 0, 0));
}
return tinted;
}
// Blocks without a texture, like custom blocks or everything before the textures load, fall back to map colours.
private int[] resolveLegendColors() {
List<BlockData> untexturedBlocks = new ArrayList<>();
for (int entry = 0; entry < blocks.size(); entry++) {
BlockData block = blocks.get(entry).getBlockData(data);
if (block != null && textureByEntry[entry] == null) {
untexturedBlocks.add(block);
}
}
Map<String, Integer> mapColorsByBlock = BlockPreviewColors.colorsByBlock(untexturedBlocks);
int[] colors = new int[blocks.size()];
for (int entry = 0; entry < blocks.size(); entry++) {
BlockData block = blocks.get(entry).getBlockData(data);
if (block == null) {
colors[entry] = MISSING_BLOCK_COLOR;
} else if (legendTextureByEntry[entry] != null) {
colors[entry] = legendTextureByEntry[entry].averageColor();
} else {
colors[entry] = mapColorsByBlock.get(block.getAsString(true));
}
}
return colors;
}
private Statistics measure() {
long[] values = new long[HISTOGRAM_BINS];
long[] samplesByEntry = new long[blocks.size()];
long present = 0;
long absent = 0;
long outOfRange = 0;
double origin = -statisticsAreaBlocks() / 2D;
for (int gridX = 0; gridX < STATISTICS_GRID_SIZE; gridX++) {
for (int gridZ = 0; gridZ < STATISTICS_GRID_SIZE; gridZ++) {
double noise = sample(origin + gridX * STATISTICS_SPACING_BLOCKS, origin + gridZ * STATISTICS_SPACING_BLOCKS);
if (Double.isNaN(noise)) {
absent++;
continue;
}
present++;
if (noise < 0D || noise > 1D) {
outOfRange++;
}
values[Math.clamp((int) (noise * HISTOGRAM_BINS), 0, HISTOGRAM_BINS - 1)]++;
samplesByEntry[entryIndexForNoise(noise)]++;
}
}
double[] sharesByEntry = new double[samplesByEntry.length];
for (int entry = 0; entry < samplesByEntry.length; entry++) {
sharesByEntry[entry] = present == 0 ? 0D : samplesByEntry[entry] / (double) present;
}
long total = present + absent;
return new Statistics(values, sharesByEntry, absent / (double) total, present == 0 ? 0D : outOfRange / (double) present);
}
private List<Entry> describeEntries(double[] sharesByEntry) {
int slotCount = entryBySlot.length;
List<Entry> described = new ArrayList<>(blocks.size());
for (int entry = 0; entry < blocks.size(); entry++) {
IrisBlockData block = blocks.get(entry);
int firstSlot = firstSlotOf(entry);
int lastSlot = lastSlotOf(entry);
boolean placed = firstSlot >= 0;
described.add(new Entry(
block.getBlock(),
block.getWeight(),
block.getBlockData(data) != null,
legendColorByEntry[entry],
legendTextureByEntry[entry],
placed ? (lastSlot - firstSlot + 1) / (double) slotCount : 0D,
sharesByEntry[entry],
placed ? slotLowerBound(firstSlot, slotCount) : Double.NaN,
placed ? slotUpperBound(lastSlot, slotCount) : Double.NaN
));
}
return List.copyOf(described);
}
private int firstSlotOf(int entry) {
for (int slot = 0; slot < entryBySlot.length; slot++) {
if (entryBySlot[slot] == entry) {
return slot;
}
}
return -1;
}
private int lastSlotOf(int entry) {
for (int slot = entryBySlot.length - 1; slot >= 0; slot--) {
if (entryBySlot[slot] == entry) {
return slot;
}
}
return -1;
}
// Slots are picked by rounding, so the first and last slot only own half a step of the value range.
private static double slotLowerBound(int slot, int slotCount) {
return slot == 0 ? 0D : (slot - 0.5D) / (slotCount - 1);
}
private static double slotUpperBound(int slot, int slotCount) {
return slot == slotCount - 1 ? 1D : (slot + 0.5D) / (slotCount - 1);
}
record Entry(String block, int weight, boolean found, int color, BlockTexture texture, double weightShare,
double actualShare, double lowestNoise, double highestNoise) {
boolean isFarBelowWeight() {
return found && actualShare < weightShare / FAR_FROM_WEIGHT_FACTOR;
}
boolean isFarAboveWeight() {
return found && actualShare > weightShare * FAR_FROM_WEIGHT_FACTOR;
}
}
private record SampledBlock(int x, int z, double noise) {
}
private record Statistics(long[] histogram, double[] sharesByEntry, double absentShare, double outOfRangeShare) {
}
}
@@ -0,0 +1,107 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import java.awt.Color;
final class PaletteLayerScale implements NoiseColorScale {
static final int ABSENT_COLOR = 0x161A24;
private static final double OVERLAY_DARKEST_SHADE = 0.3D;
private final View view;
private final PaletteLayerPreview preview;
private final NoisePalette noisePalette;
private final boolean showTexturePixels;
PaletteLayerScale(View view, PaletteLayerPreview preview, NoisePalette noisePalette, boolean showTexturePixels) {
this.view = view;
this.preview = preview;
this.noisePalette = noisePalette;
this.showTexturePixels = showTexturePixels;
}
@Override
public int colorAt(double value, double worldX, double worldZ) {
if (view == View.NOISE) {
return noisePalette.colorFinite(value);
}
int blockColor = preview.blockColorAt(value, worldX, worldZ, showTexturePixels);
return view == View.BLOCKS ? blockColor : BlockPreviewColors.shade(blockColor, overlayShade(value));
}
@Override
public int colorFinite(double value) {
return switch (view) {
case BLOCKS -> preview.colorForNoise(value);
case NOISE -> noisePalette.colorFinite(value);
case BLOCKS_SHADED_BY_NOISE -> BlockPreviewColors.shade(preview.colorForNoise(value), overlayShade(value));
};
}
@Override
public int colorInvalid() {
return ABSENT_COLOR;
}
@Override
public int colorInvalidAt(double worldX, double worldZ) {
return view == View.NOISE ? ABSENT_COLOR : preview.bareColorAt(worldX, worldZ, showTexturePixels);
}
@Override
public Color displayColorNormalized(double normalized) {
return new Color(colorFinite(minimum() + normalized * (maximum() - minimum())));
}
@Override
public String label() {
return view == View.NOISE ? noisePalette.label() : view.toString();
}
@Override
public double minimum() {
return view == View.NOISE ? noisePalette.minimum() : 0D;
}
@Override
public double maximum() {
return view == View.NOISE ? noisePalette.maximum() : 1D;
}
private static double overlayShade(double value) {
return OVERLAY_DARKEST_SHADE + (1D - OVERLAY_DARKEST_SHADE) * Math.clamp(value, 0D, 1D);
}
enum View {
BLOCKS("Blocks"),
NOISE("Noise"),
BLOCKS_SHADED_BY_NOISE("Both");
private final String label;
View(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
}
@@ -0,0 +1,527 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.core.gui.PaletteCatalog.BiomeEntry;
import com.volmit.iris.core.gui.PaletteCatalog.BiomeGroup;
import com.volmit.iris.core.gui.PaletteCatalog.RegionEntry;
import com.volmit.iris.core.gui.PlayerLocationTracker.PlayerLocation;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.JTree;
import javax.swing.ListSelectionModel;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Component;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
final class PaletteNavigator extends JPanel {
private static final long serialVersionUID = -6402313718430917284L;
private static final String HERE_CARD = "here";
private static final String OVERVIEW_CARD = "overview";
private static final int MESSAGE_WRAP_WIDTH_PIXELS = 190;
private final Listener listener;
private final CardLayout cards = new CardLayout();
private final JPanel cardPanel = new JPanel(cards);
private final JLabel regionLabel = new JLabel();
private final DefaultListModel<HereRow> hereRows = new DefaultListModel<>();
private final JList<HereRow> hereList = new JList<>(hereRows);
private final JTextField searchField = new JTextField();
private final DefaultTreeModel treeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
private final JTree overviewTree = new JTree(treeModel);
private PaletteCatalog catalog;
private PlayerLocation location;
private BiomePalette selectedPalette;
private boolean updatingSelection;
private boolean untracked;
PaletteNavigator(Listener listener) {
super(new BorderLayout());
this.listener = listener;
cardPanel.add(buildHereCard(), HERE_CARD);
cardPanel.add(buildOverviewCard(), OVERVIEW_CARD);
add(buildTabBar(), BorderLayout.NORTH);
add(cardPanel, BorderLayout.CENTER);
refreshHere();
}
void showCatalog(PaletteCatalog nextCatalog) {
catalog = nextCatalog;
rebuildOverview();
refreshHere();
}
void showLocation(PlayerLocation nextLocation) {
location = nextLocation;
refreshHere();
}
// Without a player there is nowhere to call "here", so the Here tab explains how to get one instead.
void showUntracked() {
untracked = true;
refreshHere();
}
void markSelected(BiomePalette palette) {
selectedPalette = palette;
updatingSelection = true;
try {
selectInHereList();
selectInTree();
} finally {
updatingSelection = false;
}
}
private JPanel buildTabBar() {
JPanel bar = new JPanel(new GridLayout(1, 2, 0, 0));
bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, StudioTheme.SEPARATOR));
ButtonGroup group = new ButtonGroup();
JToggleButton here = createTab("Here", HERE_CARD, group);
JToggleButton overview = createTab("Overview", OVERVIEW_CARD, group);
bar.add(here);
bar.add(overview);
here.doClick();
return bar;
}
private JToggleButton createTab(String text, String card, ButtonGroup group) {
JToggleButton tab = StudioToolbar.tab(text);
tab.addActionListener(event -> cards.show(cardPanel, card));
group.add(tab);
return tab;
}
private JPanel buildHereCard() {
JPanel card = new JPanel(new BorderLayout());
JPanel location = new JPanel(new GridLayout(0, 1, 0, StudioTheme.scale(3)));
location.setBorder(StudioTheme.emptyBorder(9, 12, 5, 12));
location.add(regionLabel);
hereList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
hereList.setCellRenderer(new HereRowRenderer());
hereList.addListSelectionListener(event -> {
HereRow row = hereList.getSelectedValue();
if (!event.getValueIsAdjusting() && !updatingSelection && row != null && row.biomeKey() != null) {
choose(row.biomeKey(), row.palette());
}
});
card.add(location, BorderLayout.NORTH);
card.add(StudioToolbar.scroll(hereList), BorderLayout.CENTER);
return card;
}
private JPanel buildOverviewCard() {
JPanel card = new JPanel(new BorderLayout());
StudioToolbar.makeSearchField(searchField, "Search biomes, snippets or blocks...");
searchField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent event) {
rebuildOverview();
}
@Override
public void removeUpdate(DocumentEvent event) {
rebuildOverview();
}
@Override
public void changedUpdate(DocumentEvent event) {
rebuildOverview();
}
});
overviewTree.setRootVisible(false);
overviewTree.setShowsRootHandles(true);
overviewTree.setRowHeight(StudioTheme.scale(24));
overviewTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
overviewTree.setCellRenderer(new OverviewRenderer());
overviewTree.addTreeSelectionListener(event -> {
if (!updatingSelection && nodeAt(event.getPath()) instanceof OverviewNode node && node.biomeKey() != null) {
choose(node.biomeKey(), node.palette());
}
});
card.add(StudioToolbar.padded(searchField, 8, 8, 8, 8), BorderLayout.NORTH);
card.add(StudioToolbar.scroll(overviewTree), BorderLayout.CENTER);
return card;
}
// Choosing a biome rather than one of its palettes opens its first palette.
private void choose(String biomeKey, BiomePalette palette) {
BiomeEntry biome = catalog == null ? null : catalog.biome(biomeKey);
BiomePalette chosen = palette != null ? palette : biome == null ? null : biome.firstPalette();
if (chosen != null) {
listener.onPaletteChosen(chosen);
}
}
private void refreshHere() {
updatingSelection = true;
try {
hereRows.clear();
if (untracked) {
showHereMessage("Run /iris std pal in-game to see the palettes where you stand.");
return;
}
if (location == null || catalog == null) {
showHereMessage("Finding where you are...");
return;
}
switch (location.status()) {
case OFFLINE -> showHereMessage("You are offline.");
case IN_OTHER_WORLD -> showHereMessage("You left the world this window shows.");
case IN_REGION -> showHereRows(location);
}
selectInHereList();
} finally {
updatingSelection = false;
}
}
private void showHereMessage(String message) {
regionLabel.setText("<html><body style='width:" + StudioTheme.scale(MESSAGE_WRAP_WIDTH_PIXELS) + "px'>" + message + "</body></html>");
}
private void showHereRows(PlayerLocation here) {
RegionEntry region = catalog.region(here.regionKey());
regionLabel.setText(labeled("Region", region == null ? here.regionKey() : region.name(), here.regionKey()));
addBiomePalettes("Surface biome here", here.surfaceBiomeKey());
if (here.isBelowSurfaceBiome()) {
addBiomePalettes("Underground biome at your height", here.biomeAtPositionKey());
}
if (region == null) {
return;
}
hereRows.addElement(HereRow.header("Biomes in this region"));
for (String biomeKey : region.allBiomeKeys()) {
addRegionBiome(biomeKey, 0, new HashSet<>(), here);
}
}
private void addBiomePalettes(String title, String biomeKey) {
BiomeEntry biome = catalog.biome(biomeKey);
if (biome == null) {
return;
}
hereRows.addElement(HereRow.header(title));
hereRows.addElement(HereRow.biome(biomeText(biome, childOfText(biome)), biomeKey, 0));
addPaletteRows(biome, 0);
}
private void addPaletteRows(BiomeEntry biome, int nesting) {
for (PaletteCatalog.PaletteSummary summary : biome.palettes()) {
hereRows.addElement(HereRow.palette(summary.text(), summary.palette(), nesting));
}
}
// Children sit under the biome they grow inside of, so a child biome is found where it actually appears.
private void addRegionBiome(String biomeKey, int nesting, Set<String> ancestors, PlayerLocation here) {
BiomeEntry biome = catalog.biome(biomeKey);
if (biome == null || !ancestors.add(biomeKey)) {
return;
}
boolean standingHere = biomeKey.equals(here.surfaceBiomeKey()) || biomeKey.equals(here.biomeAtPositionKey());
hereRows.addElement(HereRow.biome(biomeText(biome, standingHere ? "you are here" : null), biomeKey, nesting));
addPaletteRows(biome, nesting);
for (String childKey : biome.childKeys()) {
addRegionBiome(childKey, nesting + 1, new HashSet<>(ancestors), here);
}
}
private String childOfText(BiomeEntry biome) {
if (biome.parentKeys().isEmpty()) {
return null;
}
List<String> parentNames = biome.parentKeys().stream()
.map(key -> catalog.biome(key) == null ? key : catalog.biome(key).name())
.toList();
return "child of " + String.join(", ", parentNames);
}
private void rebuildOverview() {
if (catalog == null) {
return;
}
String filter = searchField.getText().trim().toLowerCase(Locale.ROOT);
DefaultMutableTreeNode root = new DefaultMutableTreeNode();
for (RegionEntry region : catalog.regions()) {
boolean regionMatches = !filter.isEmpty() && region.name().toLowerCase(Locale.ROOT).contains(filter);
DefaultMutableTreeNode regionNode = new DefaultMutableTreeNode(new OverviewNode(region.name(), region.key(), null, null));
for (BiomeGroup group : region.groups()) {
DefaultMutableTreeNode groupNode = new DefaultMutableTreeNode();
for (String biomeKey : group.biomeKeys()) {
addBiomeNode(groupNode, biomeKey, regionMatches ? "" : filter, new HashSet<>());
}
if (groupNode.getChildCount() > 0) {
groupNode.setUserObject(new OverviewNode(group.label(), groupNode.getChildCount() + " biomes", null, null));
regionNode.add(groupNode);
}
}
if (regionNode.getChildCount() > 0) {
root.add(regionNode);
}
}
DefaultMutableTreeNode outside = new DefaultMutableTreeNode();
for (String biomeKey : catalog.biomesOutsideRegions()) {
addBiomeNode(outside, biomeKey, filter, new HashSet<>());
}
if (outside.getChildCount() > 0) {
outside.setUserObject(new OverviewNode("Not in any region", outside.getChildCount() + " biomes", null, null));
root.add(outside);
}
addSnippetNodes(root, filter);
updatingSelection = true;
try {
treeModel.setRoot(root);
if (!filter.isEmpty()) {
expandDownToBiomes(root);
}
selectInTree();
} finally {
updatingSelection = false;
}
}
private void addBiomeNode(DefaultMutableTreeNode parent, String biomeKey, String filter, Set<String> ancestors) {
BiomeEntry biome = catalog.biome(biomeKey);
if (biome == null || !ancestors.add(biomeKey)) {
return;
}
DefaultMutableTreeNode biomeNode = new DefaultMutableTreeNode(new OverviewNode(biome.name(), biomeKey, biomeKey, null));
for (PaletteCatalog.PaletteSummary summary : biome.palettes()) {
biomeNode.add(new DefaultMutableTreeNode(new OverviewNode(summary.text(), null, biomeKey, summary.palette())));
}
for (String childKey : biome.childKeys()) {
addBiomeNode(biomeNode, childKey, filter, new HashSet<>(ancestors));
}
boolean hasMatchingChild = biomeNode.getChildCount() > biome.palettes().size();
if (filter.isEmpty() || biome.matches(filter) || hasMatchingChild) {
parent.add(biomeNode);
}
}
// Snippets sit below the regions, so a palette picked elsewhere is still found in its biome first.
private void addSnippetNodes(DefaultMutableTreeNode root, String filter) {
for (BiomePalette.SnippetFolder folder : BiomePalette.SnippetFolder.values()) {
DefaultMutableTreeNode folderNode = new DefaultMutableTreeNode();
catalog.snippets().stream()
.filter(snippet -> snippet.folder() == folder && (filter.isEmpty() || snippet.matches(filter)))
.forEach(snippet -> folderNode.add(snippetNode(snippet)));
if (folderNode.getChildCount() > 0) {
folderNode.setUserObject(new OverviewNode(folder.label() + " snippets", folderNode.getChildCount() + " files", null, null));
root.add(folderNode);
}
}
}
// Picking a snippet opens it where the first biome using it places it; its children open the other uses.
private DefaultMutableTreeNode snippetNode(PaletteCatalog.SnippetEntry snippet) {
if (snippet.uses().isEmpty()) {
return new DefaultMutableTreeNode(new OverviewNode(snippet.path(), "not used by any biome", null, null));
}
BiomePalette firstUse = snippet.uses().get(0);
String usedBy = snippet.uses().size() == 1 ? "used once" : "used " + snippet.uses().size() + " times";
DefaultMutableTreeNode node = new DefaultMutableTreeNode(new OverviewNode(snippet.path() + " · " + snippet.blocks(),
usedBy, firstUse.biomeKey(), firstUse));
for (BiomePalette use : snippet.uses()) {
BiomeEntry biome = catalog.biome(use.biomeKey());
String biomeName = biome == null ? use.biomeKey() : biome.name();
node.add(new DefaultMutableTreeNode(new OverviewNode(biomeName + " · " + use.lowercaseLabel(), use.biomeKey(),
use.biomeKey(), use)));
}
return node;
}
private void expandDownToBiomes(DefaultMutableTreeNode root) {
Enumeration<?> nodes = root.breadthFirstEnumeration();
while (nodes.hasMoreElements()) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) nodes.nextElement();
if (node.getUserObject() instanceof OverviewNode overview && overview.biomeKey() != null) {
overviewTree.expandPath(new TreePath(((DefaultMutableTreeNode) node.getParent()).getPath()));
}
}
}
private void selectInTree() {
if (selectedNodeShowsSelectedPalette()) {
return;
}
DefaultMutableTreeNode match = findLayerNode((DefaultMutableTreeNode) treeModel.getRoot());
if (match == null) {
overviewTree.clearSelection();
return;
}
TreePath path = new TreePath(match.getPath());
overviewTree.setSelectionPath(path);
overviewTree.makeVisible(path);
Rectangle row = overviewTree.getPathBounds(path);
if (row != null) {
overviewTree.scrollRectToVisible(new Rectangle(0, row.y, 1, row.height));
}
}
// The same palette can be listed under its biome and under its snippet; the row the user picked stays picked.
private boolean selectedNodeShowsSelectedPalette() {
return nodeAt(overviewTree.getSelectionPath()) instanceof OverviewNode node
&& node.palette() != null
&& node.palette().equals(selectedPalette);
}
private DefaultMutableTreeNode findLayerNode(DefaultMutableTreeNode root) {
Enumeration<?> nodes = root.depthFirstEnumeration();
while (nodes.hasMoreElements()) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) nodes.nextElement();
if (node.getUserObject() instanceof OverviewNode overview
&& overview.palette() != null
&& overview.palette().equals(selectedPalette)) {
return node;
}
}
return null;
}
private void selectInHereList() {
for (int index = 0; index < hereRows.size(); index++) {
HereRow row = hereRows.get(index);
if (row.palette() != null && row.palette().equals(selectedPalette)) {
hereList.setSelectedIndex(index);
return;
}
}
hereList.clearSelection();
}
private static Object nodeAt(TreePath path) {
return path == null ? null : ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
}
private static String biomeText(BiomeEntry biome, String note) {
String noteText = note == null ? "" : " · " + escape(note);
return "<html>" + escape(biome.name()) + noteText + " <font color='" + StudioTheme.SECONDARY_TEXT.toHtml() + "'>" + escape(biome.key())
+ "</font></html>";
}
private static String labeled(String title, String name, String key) {
return "<html><font color='" + StudioTheme.SECONDARY_TEXT.toHtml() + "'>" + title + "</font>&nbsp; " + escape(name)
+ " <font color='" + StudioTheme.SECONDARY_TEXT.toHtml() + "'>" + escape(key) + "</font></html>";
}
private static String escape(String text) {
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
}
interface Listener {
void onPaletteChosen(BiomePalette palette);
}
/**
* @param nesting how many parent biomes deep the row's biome sits
*/
private record HereRow(String text, String biomeKey, BiomePalette palette, int nesting, boolean header) {
private static HereRow header(String text) {
return new HereRow(text, null, null, 0, true);
}
private static HereRow biome(String text, String biomeKey, int nesting) {
return new HereRow(text, biomeKey, null, nesting, false);
}
private static HereRow palette(String text, BiomePalette palette, int nesting) {
return new HereRow(text, palette.biomeKey(), palette, nesting, false);
}
}
private record OverviewNode(String label, String detail, String biomeKey, BiomePalette palette) {
private String html() {
if (detail == null) {
return escape(label);
}
return "<html>" + escape(label) + " <font color='" + StudioTheme.SECONDARY_TEXT.toHtml() + "'>" + escape(detail) + "</font></html>";
}
}
private static final class HereRowRenderer extends DefaultListCellRenderer {
private static final long serialVersionUID = 2785341117408251320L;
private static final int HEADER_INDENT = 11;
private static final int BIOME_INDENT = 20;
private static final int PALETTE_INDENT = 34;
private static final int CHILD_INDENT = 16;
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean selected, boolean focus) {
HereRow row = (HereRow) value;
boolean selectable = row.biomeKey() != null;
super.getListCellRendererComponent(list, row.header() ? row.text().toUpperCase(Locale.ROOT) : row.text(), index,
selected && selectable, false);
setFont(row.header() ? StudioTheme.sectionFont() : StudioTheme.font());
if (row.header()) {
setForeground(StudioTheme.MUTED_TEXT);
}
int indent = row.header() ? HEADER_INDENT
: (row.palette() != null ? PALETTE_INDENT : BIOME_INDENT) + row.nesting() * CHILD_INDENT;
setBorder(StudioTheme.emptyBorder(row.header() ? 11 : 4, indent, 4, 10));
return this;
}
}
private static final class OverviewRenderer extends DefaultTreeCellRenderer {
private static final long serialVersionUID = -1950176034017718227L;
private OverviewRenderer() {
setLeafIcon(null);
setOpenIcon(null);
setClosedIcon(null);
}
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded,
boolean leaf, int row, boolean focus) {
Object userObject = ((DefaultMutableTreeNode) value).getUserObject();
String text = userObject instanceof OverviewNode node ? node.html() : "";
super.getTreeCellRendererComponent(tree, text, selected, expanded, leaf, row, false);
boolean groupsOthers = userObject instanceof OverviewNode node && node.biomeKey() == null && !leaf;
setFont(groupsOthers ? StudioTheme.boldFont() : StudioTheme.font());
return this;
}
}
}
@@ -0,0 +1,124 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.Iris;
import com.volmit.iris.core.tools.IrisToolbelt;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisBiome;
import com.volmit.iris.engine.object.IrisRegion;
import com.volmit.iris.engine.platform.PlatformChunkGenerator;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
import java.awt.EventQueue;
import java.util.UUID;
final class PlayerLocationTracker {
private static final long CHECK_INTERVAL_TICKS = 10L;
private final UUID playerId;
private final UUID worldId;
private final Listener listener;
private BukkitTask task;
private PlayerLocation lastLocation;
private PlayerLocationTracker(UUID playerId, UUID worldId, Listener listener) {
this.playerId = playerId;
this.worldId = worldId;
this.listener = listener;
}
static PlayerLocationTracker follow(UUID playerId, UUID worldId, Listener listener) {
PlayerLocationTracker tracker = new PlayerLocationTracker(playerId, worldId, listener);
tracker.task = Bukkit.getScheduler().runTaskTimer(Iris.instance, tracker::checkLocation, 0L, CHECK_INTERVAL_TICKS);
return tracker;
}
void stop() {
if (task != null) {
task.cancel();
}
}
private void checkLocation() {
PlayerLocation location = locate();
if (location.equals(lastLocation)) {
return;
}
lastLocation = location;
EventQueue.invokeLater(() -> listener.onLocationChanged(location));
}
private PlayerLocation locate() {
Player player = Bukkit.getPlayer(playerId);
if (player == null) {
return PlayerLocation.OFFLINE;
}
if (player.getWorld() == null || !player.getWorld().getUID().equals(worldId)) {
return PlayerLocation.IN_OTHER_WORLD;
}
Engine engine = engineAt(player.getLocation());
if (engine == null) {
return PlayerLocation.IN_OTHER_WORLD;
}
Location position = player.getLocation();
IrisRegion region = engine.getRegion(position);
String surfaceBiome = keyOf(engine.getSurfaceBiome(position.getBlockX(), position.getBlockZ()));
String biomeAtPosition = keyOf(engine.getBiomeOrMantle(position));
if (region == null || region.getLoadKey() == null || surfaceBiome == null) {
return PlayerLocation.IN_OTHER_WORLD;
}
return new PlayerLocation(Status.IN_REGION, region.getLoadKey(), surfaceBiome, biomeAtPosition);
}
private static String keyOf(IrisBiome biome) {
return biome == null ? null : biome.getLoadKey();
}
private static Engine engineAt(Location location) {
if (location.getWorld() == null || !IrisToolbelt.isIrisWorld(location.getWorld())) {
return null;
}
PlatformChunkGenerator generator = IrisToolbelt.access(location.getWorld());
return generator == null ? null : generator.getEngine();
}
enum Status {
OFFLINE,
IN_OTHER_WORLD,
IN_REGION
}
// Biomes are 3D: underground, the biome at the player's position can differ from the surface biome above it.
record PlayerLocation(Status status, String regionKey, String surfaceBiomeKey, String biomeAtPositionKey) {
static final PlayerLocation OFFLINE = new PlayerLocation(Status.OFFLINE, null, null, null);
static final PlayerLocation IN_OTHER_WORLD = new PlayerLocation(Status.IN_OTHER_WORLD, null, null, null);
boolean isBelowSurfaceBiome() {
return biomeAtPositionKey != null && !biomeAtPositionKey.equals(surfaceBiomeKey);
}
}
interface Listener {
void onLocationChanged(PlayerLocation location);
}
}
@@ -40,7 +40,10 @@ import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
@@ -53,7 +56,7 @@ public class PregeneratorJob implements PregenListener {
private static final Color COLOR_NETWORK_GENERATING = parseColor("#836b8c");
private static final Color COLOR_GENERATED = parseColor("#65c295");
private static final Color COLOR_CLEANED = parseColor("#34eb93");
public static PregeneratorJob instance;
private static final AtomicReference<PregeneratorJob> instance = new AtomicReference<>();
private final MemoryMonitor monitor;
private final PregenTask task;
private final boolean saving;
@@ -64,67 +67,83 @@ public class PregeneratorJob implements PregenListener {
private final Position2 max;
private final ChronoLatch cl = new ChronoLatch(TimeUnit.MINUTES.toMillis(1));
private final Engine engine;
private final ExecutorService service;
private JFrame frame;
private PregenRenderer renderer;
private int rgc = 0;
private String[] info;
public PregeneratorJob(PregenTask task, PregeneratorMethod method, Engine engine) {
instance.updateAndGet(old -> {
if (old != null) {
old.pregenerator.close();
old.close();
}
return this;
});
this.engine = engine;
instance = this;
monitor = new MemoryMonitor(50);
saving = false;
info = new String[]{"Initializing..."};
this.task = task;
this.pregenerator = new IrisPregenerator(task, method, this);
max = new Position2(0, 0);
min = new Position2(0, 0);
task.iterateRegions((xx, zz) -> {
min.setX(Math.min(xx << 5, min.getX()));
min.setZ(Math.min(zz << 5, min.getZ()));
max.setX(Math.max((xx << 5) + 31, max.getX()));
max.setZ(Math.max((zz << 5) + 31, max.getZ()));
min = new Position2(Integer.MAX_VALUE, Integer.MAX_VALUE);
task.iterateAllChunks((xx, zz) -> {
min.setX(Math.min(xx, min.getX()));
min.setZ(Math.min(zz, min.getZ()));
max.setX(Math.max(xx, max.getX()));
max.setZ(Math.max(zz, max.getZ()));
});
if (IrisSettings.get().getGui().isUseServerLaunchedGuis()) {
if (IrisSettings.get().getGui().isUseServerLaunchedGuis() && task.isGui()) {
open();
}
J.a(this.pregenerator::start, 20);
var t = new Thread(() -> {
J.sleep(1000);
this.pregenerator.start();
}, "Iris Pregenerator");
t.setPriority(Thread.MIN_PRIORITY);
t.start();
service = Executors.newVirtualThreadPerTaskExecutor();
}
public static boolean shutdownInstance() {
if (instance == null) {
PregeneratorJob inst = instance.get();
if (inst == null) {
return false;
}
J.a(() -> instance.pregenerator.close());
J.a(inst.pregenerator::close);
return true;
}
public static PregeneratorJob getInstance() {
return instance;
return instance.get();
}
public static boolean pauseResume() {
if (instance == null) {
PregeneratorJob inst = instance.get();
if (inst == null) {
return false;
}
if (isPaused()) {
instance.pregenerator.resume();
inst.pregenerator.resume();
} else {
instance.pregenerator.pause();
inst.pregenerator.pause();
}
return true;
}
public static boolean isPaused() {
if (instance == null) {
PregeneratorJob inst = instance.get();
if (inst == null) {
return true;
}
return instance.paused();
return inst.paused();
}
private static Color parseColor(String c) {
@@ -154,7 +173,7 @@ public class PregeneratorJob implements PregenListener {
}
public void drawRegion(int x, int z, Color color) {
J.a(() -> PregenTask.iterateRegion(x, z, (xx, zz) -> {
J.a(() -> task.iterateChunks(x, z, (xx, zz) -> {
draw(xx, zz, color);
J.sleep(3);
}));
@@ -174,7 +193,7 @@ public class PregeneratorJob implements PregenListener {
J.a(() -> {
pregenerator.close();
close();
instance = null;
instance.compareAndSet(this, null);
});
}
@@ -193,7 +212,7 @@ public class PregeneratorJob implements PregenListener {
public void open() {
J.a(() -> {
try {
frame = new JFrame("Pregen View");
frame = GuiHost.createFrame("Pregen View");
renderer = new PregenRenderer();
frame.addKeyListener(renderer);
renderer.l = new ReentrantLock();
@@ -214,10 +233,10 @@ public class PregeneratorJob implements PregenListener {
}
@Override
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, int generated, int totalChunks, int chunksRemaining, long eta, long elapsed, String method) {
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) {
info = new String[]{
(paused() ? "PAUSED" : (saving ? "Saving... " : "Generating")) + " " + Form.f(generated) + " of " + Form.f(totalChunks) + " (" + Form.pc(percent, 0) + " Complete)",
"Speed: " + Form.f(chunksPerSecond, 0) + " Chunks/s, " + Form.f(regionsPerMinute, 1) + " Regions/m, " + Form.f(chunksPerMinute, 0) + " Chunks/m",
"Speed: " + (cached ? "Cached " : "") + Form.f(chunksPerSecond, 0) + " Chunks/s, " + Form.f(regionsPerMinute, 1) + " Regions/m, " + Form.f(chunksPerMinute, 0) + " Chunks/m",
Form.duration(eta, 2) + " Remaining " + " (" + Form.duration(elapsed, 2) + " Elapsed)",
"Generation Method: " + method,
"Memory: " + Form.memSize(monitor.getUsedBytes(), 2) + " (" + Form.pc(monitor.getUsagePercent(), 0) + ") Pressure: " + Form.memSize(monitor.getPressure(), 0) + "/s",
@@ -235,13 +254,16 @@ public class PregeneratorJob implements PregenListener {
}
@Override
public void onChunkGenerated(int x, int z) {
if (engine != null) {
draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8));
return;
}
public void onChunkGenerated(int x, int z, boolean cached) {
if (renderer == null || frame == null || !frame.isVisible()) return;
service.submit(() -> {
if (engine != null) {
draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8));
return;
}
draw(x, z, COLOR_GENERATED);
draw(x, z, COLOR_GENERATED);
});
}
@Override
@@ -299,8 +321,9 @@ public class PregeneratorJob implements PregenListener {
@Override
public void onClose() {
close();
instance = null;
instance.compareAndSet(this, null);
whenDone.forEach(Runnable::run);
service.shutdownNow();
}
@Override
@@ -0,0 +1,268 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import javax.swing.CellRendererPane;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JLayer;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.plaf.LayerUI;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.tree.TreePath;
import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
/**
* Keeps the folders around the top row of a scrolled tree pinned at the top, so a long folder never hides where it
* is. The next folder pushes the last header up as it arrives, and clicking a header jumps to its folder.
*/
final class StickyFolderHeaders extends LayerUI<JScrollPane> {
private static final long serialVersionUID = -2205474187531254372L;
private static final int SHADOW_HEIGHT = 4;
private static final Color SHADOW = new ThemeColor(new Color(0, 0, 0, 28), new Color(0, 0, 0, 90));
private static final Color NO_SHADOW = new Color(0, 0, 0, 0);
// The folder handle sits one indent to the left of the folder's name, as the tree draws it.
private static final int HANDLE_NUDGE = 1;
private final JTree tree;
private final CellRendererPane rendererPane = new CellRendererPane();
private transient JLayer<JScrollPane> layer;
private int hoveredHeader = -1;
private StickyFolderHeaders(JTree tree) {
this.tree = tree;
}
static JLayer<JScrollPane> around(JTree tree, JScrollPane scrollPane) {
// Copying the scrolled pixels would drag the pinned headers along, so every scroll repaints instead.
scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
StickyFolderHeaders headers = new StickyFolderHeaders(tree);
JLayer<JScrollPane> layer = new JLayer<>(scrollPane, headers);
scrollPane.getViewport().addChangeListener(event -> layer.repaint());
return layer;
}
@Override
@SuppressWarnings("unchecked")
public void installUI(JComponent component) {
super.installUI(component);
layer = (JLayer<JScrollPane>) component;
layer.setLayerEventMask(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
tree.add(rendererPane);
}
@Override
public void uninstallUI(JComponent component) {
layer.setLayerEventMask(0);
tree.remove(rendererPane);
layer = null;
super.uninstallUI(component);
}
@Override
public void paint(Graphics graphics, JComponent component) {
super.paint(graphics, component);
List<Header> headers = pinnedHeaders();
if (headers.isEmpty()) {
return;
}
Graphics2D canvas = (Graphics2D) graphics.create();
try {
Rectangle viewport = viewportBounds();
canvas.clipRect(viewport.x, viewport.y, viewport.width, viewport.height);
for (int index = 0; index < headers.size(); index++) {
paintHeader(canvas, headers.get(index), index == hoveredHeader, viewport);
}
Header last = headers.get(headers.size() - 1);
int shadowTop = viewport.y + last.top() + tree.getRowHeight();
canvas.setPaint(new GradientPaint(0, shadowTop, SHADOW, 0, shadowTop + StudioTheme.scale(SHADOW_HEIGHT), NO_SHADOW));
canvas.fillRect(viewport.x, shadowTop, viewport.width, StudioTheme.scale(SHADOW_HEIGHT));
} finally {
canvas.dispose();
}
}
private void paintHeader(Graphics2D canvas, Header header, boolean hovered, Rectangle viewport) {
int rowHeight = tree.getRowHeight();
int top = viewport.y + header.top();
Rectangle row = tree.getRowBounds(header.row());
int left = viewport.x + row.x - scrolledView().x;
canvas.setColor(hovered ? StudioTheme.HOVER_BACKGROUND : tree.getBackground());
canvas.fillRect(viewport.x, top, viewport.width, rowHeight);
paintFolderHandle(canvas, left, top + rowHeight / 2);
Component renderer = tree.getCellRenderer().getTreeCellRendererComponent(
tree, header.path().getLastPathComponent(), false, true, false, header.row(), false);
rendererPane.paintComponent(canvas, renderer, tree, left, top, viewport.x + viewport.width - left, rowHeight, true);
}
private void paintFolderHandle(Graphics2D canvas, int folderLeft, int middleY) {
Icon handle = UIManager.getIcon("Tree.expandedIcon");
if (handle == null || !(tree.getUI() instanceof BasicTreeUI treeUI)) {
return;
}
int middleX = folderLeft - treeUI.getRightChildIndent() + HANDLE_NUDGE;
handle.paintIcon(tree, canvas, middleX - handle.getIconWidth() / 2, middleY - handle.getIconHeight() / 2);
}
@Override
protected void processMouseEvent(MouseEvent event, JLayer<? extends JScrollPane> source) {
if (event.getID() == MouseEvent.MOUSE_EXITED) {
hover(-1);
return;
}
int index = headerAt(event);
if (index < 0) {
return;
}
event.consume();
if (event.getID() == MouseEvent.MOUSE_CLICKED && SwingUtilities.isLeftMouseButton(event)) {
jumpTo(pinnedHeaders().get(index), index);
}
}
@Override
protected void processMouseMotionEvent(MouseEvent event, JLayer<? extends JScrollPane> source) {
hover(headerAt(event));
}
private void hover(int index) {
if (index == hoveredHeader) {
return;
}
hoveredHeader = index;
tree.setCursor(index >= 0 ? Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) : null);
layer.repaint();
}
// The folder lands just below the headers of the folders around it.
private void jumpTo(Header header, int level) {
Rectangle row = tree.getRowBounds(header.row());
JViewport viewport = layer.getView().getViewport();
int top = Math.max(0, row.y - level * tree.getRowHeight());
int bottomLimit = Math.max(0, tree.getHeight() - viewport.getExtentSize().height);
viewport.setViewPosition(new Point(viewport.getViewPosition().x, Math.min(top, bottomLimit)));
}
private int headerAt(MouseEvent event) {
if (layer == null || event.getComponent() == null) {
return -1;
}
Point point = SwingUtilities.convertPoint(event.getComponent(), event.getPoint(), layer);
Rectangle viewport = viewportBounds();
if (!viewport.contains(point)) {
return -1;
}
List<Header> headers = pinnedHeaders();
for (int index = headers.size() - 1; index >= 0; index--) {
int top = viewport.y + headers.get(index).top();
if (point.y >= top && point.y < top + tree.getRowHeight()) {
return index;
}
}
return -1;
}
/**
* For each depth, the folder holding the row just under the headers above it, as long as that folder's own row
* has scrolled out of sight.
*/
private List<Header> pinnedHeaders() {
List<Header> headers = new ArrayList<>();
int rowHeight = tree.getRowHeight();
if (layer == null || rowHeight <= 0 || tree.getRowCount() == 0) {
return headers;
}
Rectangle view = scrolledView();
for (int level = 0; ; level++) {
int slotTop = level * rowHeight;
int row = tree.getClosestRowForLocation(view.x, view.y + slotTop);
TreePath path = row < 0 ? null : tree.getPathForRow(row);
if (path == null || !isInsideFolderAt(path, level)) {
return headers;
}
TreePath folder = ancestorAt(path, level);
Rectangle lastRow = tree.getRowBounds(lastRowInside(folder, row));
int top = Math.min(slotTop, lastRow.y + lastRow.height - view.y - rowHeight);
headers.add(new Header(folder, tree.getRowForPath(folder), top));
if (top < slotTop) {
return headers;
}
}
}
// The hidden root is the path's first element, so the folder at depth 0 is its second.
private static boolean isInsideFolderAt(TreePath path, int level) {
return path.getPathCount() > level + 2;
}
private static TreePath ancestorAt(TreePath path, int level) {
TreePath ancestor = path;
while (ancestor.getPathCount() > level + 2) {
ancestor = ancestor.getParentPath();
}
return ancestor;
}
// A folder's rows sit together, so the last one is found by halving the rows after a row known to be inside.
private int lastRowInside(TreePath folder, int rowInside) {
int low = rowInside;
int high = tree.getRowCount() - 1;
while (low < high) {
int middle = (low + high + 1) >>> 1;
if (folder.isDescendant(tree.getPathForRow(middle))) {
low = middle;
} else {
high = middle - 1;
}
}
return low;
}
private Rectangle scrolledView() {
return layer.getView().getViewport().getViewRect();
}
private Rectangle viewportBounds() {
JViewport viewport = layer.getView().getViewport();
Point origin = SwingUtilities.convertPoint(viewport, 0, 0, layer);
return new Rectangle(origin.x, origin.y, viewport.getWidth(), viewport.getHeight());
}
/**
* A pinned folder, its row in the tree and how far below the top of the visible rows it is drawn.
*/
private record Header(TreePath path, int row, int top) {
}
}
@@ -0,0 +1,43 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.RoundRectangle2D;
/**
* Draws the floating cards (legends, hover details, hints) on top of a map, styled like IntelliJ's popups.
*/
final class StudioOverlay {
private StudioOverlay() {
}
static void drawCard(Graphics2D canvas, int x, int y, int width, int height) {
Object antialiasing = canvas.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
RoundRectangle2D card = new RoundRectangle2D.Double(x + 0.5D, y + 0.5D, width - 1D, height - 1D,
StudioTheme.scale(StudioTheme.OVERLAY_ARC), StudioTheme.scale(StudioTheme.OVERLAY_ARC));
canvas.setColor(StudioTheme.OVERLAY_BACKGROUND);
canvas.fill(card);
canvas.setColor(StudioTheme.OVERLAY_BORDER);
canvas.draw(card);
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
}
}
@@ -0,0 +1,110 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import java.awt.Dimension;
import java.util.Objects;
/**
* An IntelliJ style status bar: what is shown on the left, work in progress in the middle and where the pointer is on the right.
*/
final class StudioStatusBar extends JPanel {
private static final long serialVersionUID = 6302468216735125611L;
private static final int HEIGHT = 26;
private static final int PROGRESS_STEPS = 1_000;
private final JLabel summary = statusLabel();
private final JLabel activity = statusLabel();
private final JProgressBar progress = new JProgressBar(0, PROGRESS_STEPS);
private final JLabel position = statusLabel();
StudioStatusBar() {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setBackground(StudioTheme.PANEL_BACKGROUND);
setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(1, 0, 0, 0, StudioTheme.SEPARATOR),
StudioTheme.emptyBorder(0, 10, 0, 10)
));
setPreferredSize(StudioTheme.scaledSize(0, HEIGHT));
progress.setMaximumSize(StudioTheme.scaledSize(120, 4));
progress.setPreferredSize(StudioTheme.scaledSize(120, 4));
progress.setVisible(false);
allowShrinking(summary);
allowShrinking(activity);
add(summary);
add(Box.createHorizontalGlue());
add(activity);
add(Box.createHorizontalStrut(StudioTheme.scale(8)));
add(progress);
add(Box.createHorizontalStrut(StudioTheme.scale(18)));
add(position);
}
void showSummary(String text) {
setTextIfChanged(summary, text);
summary.setForeground(StudioTheme.SECONDARY_TEXT);
}
void showError(String text) {
setTextIfChanged(summary, text);
summary.setForeground(StudioTheme.ERROR);
}
void showActivity(String text) {
setTextIfChanged(activity, text);
}
void showProgress(double completion) {
boolean working = completion < 1D;
progress.setVisible(working);
if (working) {
progress.setValue((int) Math.round(Math.max(0D, completion) * PROGRESS_STEPS));
}
}
void showPosition(String text) {
setTextIfChanged(position, text);
}
// Long text gives way with an ellipsis instead of pushing the other parts of the bar out of view.
private static void allowShrinking(JLabel label) {
label.setMinimumSize(new Dimension(0, 0));
}
private static JLabel statusLabel() {
JLabel label = new JLabel();
label.setFont(StudioTheme.smallFont());
label.setForeground(StudioTheme.SECONDARY_TEXT);
return label;
}
// Status text changes on every mouse move, and an unchanged label shouldn't trigger a relayout.
private static void setTextIfChanged(JLabel label, String text) {
String next = text == null ? "" : text;
if (!Objects.equals(label.getText(), next)) {
label.setText(next);
}
}
}
@@ -0,0 +1,291 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.formdev.flatlaf.FlatDarkLaf;
import com.formdev.flatlaf.FlatLaf;
import com.formdev.flatlaf.FlatLightLaf;
import com.formdev.flatlaf.util.UIScale;
import com.volmit.iris.Iris;
import com.volmit.iris.core.IrisSettings;
import javax.swing.BorderFactory;
import javax.swing.UIManager;
import javax.swing.border.Border;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* The colours of IntelliJ's "New UI", light and dark, shared by FlatLaf's controls and everything the studio windows
* draw themselves. The theme follows the system unless the user picked light or dark.
*/
final class StudioTheme {
static final Color EDITOR_BACKGROUND = new ThemeColor(0xFFFFFF, 0x1E1F22);
static final Color PANEL_BACKGROUND = new ThemeColor(0xF7F8FA, 0x2B2D30);
static final Color HOVER_BACKGROUND = new ThemeColor(0xDFE1E5, 0x393B40);
static final Color INACTIVE_SELECTION = new ThemeColor(0xDFE1E5, 0x43454A);
static final Color SELECTION = new ThemeColor(0xD4E2FF, 0x2E436E);
static final Color CONTROL_BORDER = new ThemeColor(0xC9CCD6, 0x4E5157);
static final Color SEPARATOR = new ThemeColor(0xEBECF0, 0x1E1F22);
static final Color TEXT = new ThemeColor(0x000000, 0xDFE1E5);
static final ThemeColor SECONDARY_TEXT = new ThemeColor(0x6C707E, 0x9DA0A8);
static final Color MUTED_TEXT = new ThemeColor(0x818594, 0x6F737A);
static final Color ACCENT = new Color(0x3574F0);
static final Color LINK = new ThemeColor(0x315FBD, 0x548AF7);
static final Color ERROR = new ThemeColor(0xDB3B4B, 0xF75464);
static final Color WARNING = new ThemeColor(0xC27D04, 0xF2C55C);
static final Color SUCCESS = new ThemeColor(0x208A3C, 0x5FB865);
static final Color OVERLAY_BACKGROUND = new ThemeColor(new Color(0xFF, 0xFF, 0xFF, 240), new Color(0x2B, 0x2D, 0x30, 236));
static final Color OVERLAY_BORDER = new ThemeColor(0xDFE1E5, 0x43454A);
static final int OVERLAY_ARC = 10;
private static final boolean DARK_WITHOUT_SYSTEM_PREFERENCE = true;
private static final AtomicBoolean INSTALLED = new AtomicBoolean();
private static final ExecutorService SYSTEM_CHECK = Executors.newSingleThreadExecutor(runnable -> {
Thread thread = new Thread(runnable, "Iris Theme Check");
thread.setDaemon(true);
return thread;
});
private static volatile boolean dark = DARK_WITHOUT_SYSTEM_PREFERENCE;
private static volatile float pixelScale = 1F;
private static final Map<String, String> SHARED_DEFAULTS = Map.ofEntries(
Map.entry("Component.focusWidth", "1"),
Map.entry("Component.innerFocusWidth", "0"),
Map.entry("Component.arc", "8"),
Map.entry("Button.arc", "8"),
Map.entry("TextComponent.arc", "8"),
Map.entry("List.selectionArc", "6"),
Map.entry("List.selectionInsets", "0,6,0,6"),
Map.entry("Tree.selectionArc", "6"),
Map.entry("Tree.selectionInsets", "0,6,0,6"),
Map.entry("Tree.border", "0,8,0,0"),
Map.entry("ScrollBar.width", "10"),
Map.entry("ScrollBar.thumbArc", "999"),
Map.entry("ScrollBar.thumbInsets", "2,2,2,2"),
Map.entry("ScrollBar.showButtons", "false"),
Map.entry("ScrollPane.border", "0,0,0,0"),
Map.entry("SplitPane.dividerSize", "1"),
Map.entry("SplitPaneDivider.style", "plain"),
Map.entry("ProgressBar.arc", "999"),
Map.entry("ProgressBar.horizontalSize", "120,4")
);
private static final Map<String, String> DARK_DEFAULTS = withShared(Map.ofEntries(
Map.entry("@background", "#2B2D30"),
Map.entry("@foreground", "#DFE1E5"),
Map.entry("@accentColor", "#3574F0"),
Map.entry("@selectionBackground", "#2E436E"),
Map.entry("@selectionInactiveBackground", "#43454A"),
Map.entry("@disabledForeground", "#6F737A"),
Map.entry("Component.borderColor", "#4E5157"),
Map.entry("Component.focusedBorderColor", "#3574F0"),
Map.entry("Button.toolbar.hoverBackground", "#393B40"),
Map.entry("Button.toolbar.pressedBackground", "#43454A"),
Map.entry("Button.toolbar.selectedBackground", "#43454A"),
Map.entry("ToggleButton.toolbar.selectedBackground", "#43454A"),
Map.entry("ScrollBar.track", "#2B2D30"),
Map.entry("SplitPane.background", "#1E1F22"),
Map.entry("Separator.foreground", "#1E1F22"),
Map.entry("ToolTip.background", "#393B40")
));
private static final Map<String, String> LIGHT_DEFAULTS = withShared(Map.ofEntries(
Map.entry("@background", "#F7F8FA"),
Map.entry("@foreground", "#000000"),
Map.entry("@accentColor", "#3574F0"),
Map.entry("@selectionBackground", "#D4E2FF"),
Map.entry("@selectionForeground", "#000000"),
Map.entry("@selectionInactiveBackground", "#DFE1E5"),
Map.entry("@selectionInactiveForeground", "#000000"),
Map.entry("@disabledForeground", "#A8ADBD"),
Map.entry("Component.borderColor", "#C9CCD6"),
Map.entry("Component.focusedBorderColor", "#3574F0"),
Map.entry("Button.toolbar.hoverBackground", "#DFE1E5"),
Map.entry("Button.toolbar.pressedBackground", "#C9CCD6"),
Map.entry("Button.toolbar.selectedBackground", "#DFE1E5"),
Map.entry("ToggleButton.toolbar.selectedBackground", "#DFE1E5"),
Map.entry("ScrollBar.track", "#F7F8FA"),
Map.entry("SplitPane.background", "#EBECF0"),
Map.entry("Separator.foreground", "#EBECF0")
));
private StudioTheme() {
}
static void install() {
if (!INSTALLED.compareAndSet(false, true)) {
return;
}
try {
applyLookAndFeel(darkFor(mode()));
pixelScale = UIScale.getUserScaleFactor();
} catch (Throwable error) {
Iris.reportError(error);
Iris.warn("Could not apply the Iris studio theme, the studio windows will use Java's default look.");
}
}
static boolean isDark() {
return dark;
}
static Mode mode() {
return Mode.fromSetting(IrisSettings.get().getGui().getTheme());
}
/**
* Remembers the user's choice and switches every studio window to it.
*/
static void choose(Mode next) {
IrisSettings settings = IrisSettings.get();
settings.getGui().setTheme(next.settingName());
settings.forceSave();
if (next == Mode.AUTO) {
followSystem();
return;
}
switchTo(next == Mode.DARK);
}
// Studio windows call this when they come to the front, so a change made in the system's settings shows up.
static void followSystem() {
if (!INSTALLED.get() || mode() != Mode.AUTO) {
return;
}
SYSTEM_CHECK.execute(() -> {
boolean systemDark = SystemAppearance.prefersDark().orElse(DARK_WITHOUT_SYSTEM_PREFERENCE);
EventQueue.invokeLater(() -> switchTo(systemDark));
});
}
private static boolean darkFor(Mode chosen) {
return switch (chosen) {
case LIGHT -> false;
case DARK -> true;
case AUTO -> SystemAppearance.prefersDark().orElse(DARK_WITHOUT_SYSTEM_PREFERENCE);
};
}
private static void switchTo(boolean nextDark) {
if (nextDark == dark) {
return;
}
applyLookAndFeel(nextDark);
FlatLaf.updateUI();
}
private static void applyLookAndFeel(boolean nextDark) {
dark = nextDark;
FlatLaf.setGlobalExtraDefaults(nextDark ? DARK_DEFAULTS : LIGHT_DEFAULTS);
if (nextDark) {
FlatDarkLaf.setup();
} else {
FlatLightLaf.setup();
}
}
private static Map<String, String> withShared(Map<String, String> themeDefaults) {
Map<String, String> merged = new HashMap<>(SHARED_DEFAULTS);
merged.putAll(themeDefaults);
return Map.copyOf(merged);
}
/**
* FlatLaf grows fonts and its own controls to the screen's DPI; every size the studio sets itself has to grow with them.
*/
static int scale(int pixels) {
return Math.round(pixels * pixelScale);
}
static double scale(double pixels) {
return pixels * pixelScale;
}
static Dimension scaledSize(int width, int height) {
return new Dimension(scale(width), scale(height));
}
static Border emptyBorder(int top, int left, int bottom, int right) {
return BorderFactory.createEmptyBorder(scale(top), scale(left), scale(bottom), scale(right));
}
static Font font() {
return themeFont("defaultFont", Font.PLAIN, 13);
}
static Font smallFont() {
return themeFont("small.font", Font.PLAIN, 11);
}
static Font boldFont() {
return font().deriveFont(Font.BOLD);
}
static Font titleFont() {
return font().deriveFont(Font.BOLD, font().getSize2D() + 1F);
}
static Font sectionFont() {
return smallFont().deriveFont(Font.BOLD);
}
static Font monospacedFont() {
return themeFont("monospaced.font", Font.PLAIN, 12);
}
private static Font themeFont(String key, int fallbackStyle, int fallbackSize) {
Font font = UIManager.getFont(key);
return font != null ? font : new Font(Font.SANS_SERIF, fallbackStyle, fallbackSize);
}
enum Mode {
AUTO("Match system"),
LIGHT("Light"),
DARK("Dark");
private final String label;
Mode(String label) {
this.label = label;
}
String label() {
return label;
}
String settingName() {
return name().toLowerCase(Locale.ROOT);
}
static Mode fromSetting(String setting) {
for (Mode mode : values()) {
if (mode.settingName().equalsIgnoreCase(setting)) {
return mode;
}
}
return AUTO;
}
}
}
@@ -0,0 +1,144 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.icons.FlatSearchIcon;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.ScrollPaneConstants;
import java.awt.BorderLayout;
import java.awt.Insets;
/**
* Builds the IntelliJ style controls the studio windows share: a flat main toolbar, borderless toolbar buttons and search boxes.
*/
final class StudioToolbar {
private static final int TOOLBAR_HEIGHT_PADDING = 5;
private StudioToolbar() {
}
static JToolBar create() {
JToolBar toolbar = new JToolBar();
toolbar.setFloatable(false);
toolbar.setRollover(true);
toolbar.setBackground(StudioTheme.PANEL_BACKGROUND);
toolbar.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 1, 0, StudioTheme.SEPARATOR),
StudioTheme.emptyBorder(TOOLBAR_HEIGHT_PADDING, 8, TOOLBAR_HEIGHT_PADDING, 8)
));
return toolbar;
}
static JLabel label(String text) {
JLabel label = new JLabel(text);
label.setForeground(StudioTheme.SECONDARY_TEXT);
label.setBorder(StudioTheme.emptyBorder(0, 6, 0, 6));
return label;
}
static JButton button(String text, String tooltip, Runnable action) {
JButton button = toolbarButton(text, tooltip);
button.addActionListener(event -> action.run());
return button;
}
/**
* Opens a menu to pick light, dark or the system's theme.
*/
static JButton themeButton() {
JButton button = toolbarButton("Theme", "Light, dark, or whatever the system uses");
button.addActionListener(event -> themeMenu().show(button, 0, button.getHeight()));
return button;
}
private static JPopupMenu themeMenu() {
JPopupMenu menu = new JPopupMenu();
ButtonGroup choices = new ButtonGroup();
StudioTheme.Mode current = StudioTheme.mode();
for (StudioTheme.Mode mode : StudioTheme.Mode.values()) {
JRadioButtonMenuItem item = new JRadioButtonMenuItem(mode.label(), mode == current);
item.addActionListener(event -> StudioTheme.choose(mode));
choices.add(item);
menu.add(item);
}
return menu;
}
private static JButton toolbarButton(String text, String tooltip) {
JButton button = new JButton(text);
button.setToolTipText(tooltip);
button.setFocusable(false);
button.setMargin(new Insets(4, 8, 4, 8));
button.putClientProperty(FlatClientProperties.BUTTON_TYPE, FlatClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON);
return button;
}
static JToggleButton toggle(String text, String tooltip) {
JToggleButton toggle = new JToggleButton(text);
toggle.setToolTipText(tooltip);
toggle.setFocusable(false);
toggle.setMargin(new Insets(4, 8, 4, 8));
toggle.putClientProperty(FlatClientProperties.BUTTON_TYPE, FlatClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON);
return toggle;
}
// An underlined tab like IntelliJ's tool window tabs.
static JToggleButton tab(String text) {
JToggleButton tab = new JToggleButton(text);
tab.setFocusable(false);
tab.putClientProperty(FlatClientProperties.BUTTON_TYPE, FlatClientProperties.BUTTON_TYPE_TAB);
return tab;
}
static void makeSearchField(JTextField field, String placeholder) {
field.putClientProperty(FlatClientProperties.PLACEHOLDER_TEXT, placeholder);
field.putClientProperty(FlatClientProperties.TEXT_FIELD_LEADING_ICON, new FlatSearchIcon());
field.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true);
field.setToolTipText(placeholder);
}
static JPanel padded(JComponent content, int top, int left, int bottom, int right) {
JPanel panel = new JPanel(new BorderLayout());
panel.setOpaque(false);
panel.setBorder(StudioTheme.emptyBorder(top, left, bottom, right));
panel.add(content, BorderLayout.CENTER);
return panel;
}
static JScrollPane scroll(JComponent content) {
JScrollPane scrollPane = new JScrollPane(content);
scrollPane.setBorder(BorderFactory.createEmptyBorder());
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.getVerticalScrollBar().setUnitIncrement(18);
return scrollPane;
}
}
@@ -0,0 +1,110 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* Asks the operating system whether its user prefers dark or light apps, through the same settings desktop apps
* follow: the freedesktop portal or GNOME's setting on Linux, the apps theme on Windows, the appearance on macOS.
*/
final class SystemAppearance {
private static final long COMMAND_TIMEOUT_MILLIS = 2000L;
private static final String PORTAL_DARK = "uint32 1";
private static final String PORTAL_LIGHT = "uint32 2";
private SystemAppearance() {
}
/**
* Empty when the system has no preference or could not be asked.
*/
static Optional<Boolean> prefersDark() {
String os = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
if (os.contains("win")) {
return windowsPrefersDark();
}
if (os.contains("mac")) {
return macPrefersDark();
}
return linuxPrefersDark();
}
private static Optional<Boolean> linuxPrefersDark() {
Optional<String> portal = output(List.of("gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop",
"--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne",
"org.freedesktop.appearance", "color-scheme"));
if (portal.filter(answer -> answer.contains(PORTAL_DARK)).isPresent()) {
return Optional.of(true);
}
if (portal.filter(answer -> answer.contains(PORTAL_LIGHT)).isPresent()) {
return Optional.of(false);
}
Optional<String> gnome = output(List.of("gsettings", "get", "org.gnome.desktop.interface", "color-scheme"));
if (gnome.filter(scheme -> scheme.contains("prefer-dark")).isPresent()) {
return Optional.of(true);
}
if (gnome.filter(scheme -> scheme.contains("prefer-light")).isPresent()) {
return Optional.of(false);
}
return output(List.of("gsettings", "get", "org.gnome.desktop.interface", "gtk-theme"))
.map(theme -> theme.toLowerCase(Locale.ROOT).contains("dark"));
}
private static Optional<Boolean> windowsPrefersDark() {
return output(List.of("reg", "query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
"/v", "AppsUseLightTheme"))
.filter(answer -> answer.contains("AppsUseLightTheme"))
.map(answer -> answer.trim().endsWith("0x0"));
}
// The setting only exists while macOS is dark, so a failed read means light.
private static Optional<Boolean> macPrefersDark() {
return Optional.of(output(List.of("defaults", "read", "-g", "AppleInterfaceStyle"))
.filter(style -> style.trim().equalsIgnoreCase("dark"))
.isPresent());
}
private static Optional<String> output(List<String> command) {
try {
Process process = new ProcessBuilder(command).redirectErrorStream(true).start();
if (!process.waitFor(COMMAND_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
process.destroyForcibly();
return Optional.empty();
}
if (process.exitValue() != 0) {
return Optional.empty();
}
try (InputStream stream = process.getInputStream()) {
return Optional.of(new String(stream.readAllBytes(), StandardCharsets.UTF_8));
}
} catch (IOException missingCommand) {
return Optional.empty();
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
return Optional.empty();
}
}
}
@@ -0,0 +1,55 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import java.awt.Color;
/**
* A colour with a light and a dark version that always answers with the one of the current theme, like IntelliJ's
* JBColor. Components keep the same colour object across a theme switch and simply paint in the other version.
*/
final class ThemeColor extends Color {
private static final long serialVersionUID = 2286435620386470207L;
private final Color light;
private final Color dark;
ThemeColor(int lightRgb, int darkRgb) {
this(new Color(lightRgb), new Color(darkRgb));
}
ThemeColor(Color light, Color dark) {
super(dark.getRGB(), true);
this.light = light;
this.dark = dark;
}
// Every other colour query of Color, and Java2D's painting, go through this one.
@Override
public int getRGB() {
return current().getRGB();
}
String toHtml() {
return String.format("#%06X", getRGB() & 0xFFFFFF);
}
private Color current() {
return StudioTheme.isDark() ? dark : light;
}
}
@@ -0,0 +1,107 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
/**
* An affine transform built up like Minecraft's pose stack: each step applies to points before the steps added
* earlier. Rotations follow the right-hand rule, the way Minecraft turns model parts.
*/
final class Transform {
static final Transform IDENTITY = new Transform(new double[]{1D, 0D, 0D, 0D, 1D, 0D, 0D, 0D, 1D}, Vec3.ZERO);
private final double[] matrix;
private final Vec3 translation;
private Transform(double[] matrix, Vec3 translation) {
this.matrix = matrix;
this.translation = translation;
}
Transform translate(double x, double y, double z) {
return then(new Transform(IDENTITY.matrix, new Vec3(x, y, z)));
}
Transform rotateX(double degrees) {
double cos = cosine(degrees);
double sin = sine(degrees);
return then(new Transform(new double[]{1D, 0D, 0D, 0D, cos, -sin, 0D, sin, cos}, Vec3.ZERO));
}
Transform rotateY(double degrees) {
double cos = cosine(degrees);
double sin = sine(degrees);
return then(new Transform(new double[]{cos, 0D, sin, 0D, 1D, 0D, -sin, 0D, cos}, Vec3.ZERO));
}
Transform rotateZ(double degrees) {
double cos = cosine(degrees);
double sin = sine(degrees);
return then(new Transform(new double[]{cos, -sin, 0D, sin, cos, 0D, 0D, 0D, 1D}, Vec3.ZERO));
}
Transform scale(double x, double y, double z) {
return then(new Transform(new double[]{x, 0D, 0D, 0D, y, 0D, 0D, 0D, z}, Vec3.ZERO));
}
/**
* This transform applied after {@code next}.
*/
Transform then(Transform next) {
double[] product = new double[9];
for (int row = 0; row < 3; row++) {
for (int column = 0; column < 3; column++) {
product[row * 3 + column] = matrix[row * 3] * next.matrix[column]
+ matrix[row * 3 + 1] * next.matrix[3 + column]
+ matrix[row * 3 + 2] * next.matrix[6 + column];
}
}
return new Transform(product, direction(next.translation).plus(translation));
}
Vec3 point(Vec3 point) {
return direction(point).plus(translation);
}
Vec3 direction(Vec3 direction) {
return new Vec3(
matrix[0] * direction.x() + matrix[1] * direction.y() + matrix[2] * direction.z(),
matrix[3] * direction.x() + matrix[4] * direction.y() + matrix[5] * direction.z(),
matrix[6] * direction.x() + matrix[7] * direction.y() + matrix[8] * direction.z());
}
// Quarter turns stay exact, so faces turned by a blockstate still line up with the block's sides.
private static double cosine(double degrees) {
return sine(degrees + 90D);
}
private static double sine(double degrees) {
double turned = ((degrees % 360D) + 360D) % 360D;
if (turned == 0D || turned == 180D) {
return 0D;
}
if (turned == 90D) {
return 1D;
}
if (turned == 270D) {
return -1D;
}
return Math.sin(Math.toRadians(turned));
}
}
@@ -16,31 +16,32 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.util.collection;
import java.util.Collection;
import java.util.HashSet;
package com.volmit.iris.core.gui;
public class KSet<T> extends HashSet<T> {
private static final long serialVersionUID = 1L;
/**
* A position or direction in a block, in block units.
*/
record Vec3(double x, double y, double z) {
static final Vec3 ZERO = new Vec3(0D, 0D, 0D);
public KSet() {
super();
Vec3 plus(Vec3 other) {
return new Vec3(x + other.x, y + other.y, z + other.z);
}
public KSet(Collection<? extends T> c) {
super(c);
Vec3 minus(Vec3 other) {
return new Vec3(x - other.x, y - other.y, z - other.z);
}
public KSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
Vec3 times(double factor) {
return new Vec3(x * factor, y * factor, z * factor);
}
public KSet(int initialCapacity) {
super(initialCapacity);
}
public KSet<T> copy() {
return new KSet<T>(this);
double component(int axis) {
return switch (axis) {
case 0 -> x;
case 1 -> y;
default -> z;
};
}
}
@@ -0,0 +1,173 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.Iris;
import com.volmit.iris.core.gui.components.RenderType;
import com.volmit.iris.engine.IrisComplex;
import com.volmit.iris.engine.framework.Engine;
import com.volmit.iris.engine.object.IrisWorld;
import com.volmit.iris.util.format.Form;
import com.volmit.iris.util.scheduling.J;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import static com.volmit.iris.util.data.registry.Attributes.MAX_HEALTH;
final class VisionOverlay {
private final Engine engine;
private final UUID openerId;
private final AtomicBoolean playerRefreshQueued = new AtomicBoolean();
private volatile List<GuiMarker> playerMarkers = List.of();
VisionOverlay(Engine engine, UUID openerId) {
this.engine = engine;
this.openerId = openerId;
}
List<GuiMarker> lastKnownPlayers() {
queuePlayerRefresh();
return playerMarkers;
}
void requestEntities(Consumer<List<GuiMarker>> sink) {
runOnServerThread(() -> sink.accept(collectEntities()));
}
void teleport(int blockX, int blockZ) {
runOnServerThread(() -> teleportOpener(blockX, blockZ));
}
Optional<String> openInEditor(double worldX, double worldZ, RenderType type) {
IrisComplex complex = engine.getComplex();
File file = switch (type) {
case BIOME, HEIGHT ->
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
case CONTINENT -> null;
};
return Optional.ofNullable(file).map(File::getName);
}
private void queuePlayerRefresh() {
if (!playerRefreshQueued.compareAndSet(false, true)) {
return;
}
boolean scheduled = runOnServerThread(() -> {
try {
playerMarkers = collectPlayers();
} finally {
playerRefreshQueued.set(false);
}
});
if (!scheduled) {
playerRefreshQueued.set(false);
}
}
private List<GuiMarker> collectPlayers() {
List<GuiMarker> markers = new ArrayList<>();
for (Player player : engine.getWorld().getPlayers()) {
Location at = player.getLocation();
markers.add(GuiMarker.player(player.getName(), at.getX(), at.getZ()));
}
return List.copyOf(markers);
}
private List<GuiMarker> collectEntities() {
IrisWorld world = engine.getWorld();
if (!world.hasRealWorld()) {
return List.of();
}
List<GuiMarker> markers = new ArrayList<>();
for (LivingEntity entity : world.getEntitiesByClass(LivingEntity.class)) {
if (!(entity instanceof Player)) {
markers.add(marker(entity));
}
}
return List.copyOf(markers);
}
private static GuiMarker marker(LivingEntity entity) {
Location at = entity.getLocation();
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replace('_', ' '));
double maxHealth = 0D;
try {
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
} catch (Throwable ignored) {
}
return GuiMarker.entity(label, at.getX(), at.getY(), at.getZ(), entity.getHealth(), maxHealth);
}
private void teleportOpener(int blockX, int blockZ) {
IrisWorld target = engine.getWorld();
Player player = selectPlayer(target.getPlayers());
if (player == null || !target.hasRealWorld() || engine.isClosed()) {
return;
}
World world = target.realWorld();
int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
Location destination = new Location(world, blockX + 0.5D, blockY, blockZ + 0.5D);
player.teleportAsync(destination).whenComplete((success, failure) -> {
if (failure != null) {
Iris.reportError(failure);
Iris.warn("Vision could not teleport %s to %d, %d.", player.getName(), blockX, blockZ);
}
});
}
private Player selectPlayer(List<Player> players) {
if (openerId == null) {
return players.isEmpty() ? null : players.get(0);
}
for (Player player : players) {
if (openerId.equals(player.getUniqueId())) {
return player;
}
}
return null;
}
private static boolean runOnServerThread(Runnable task) {
try {
J.s(task);
return true;
} catch (Throwable e) {
Iris.reportError(e);
return false;
}
}
}
@@ -0,0 +1,649 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.volmit.iris.Iris;
import com.volmit.iris.core.gui.components.IrisRenderer;
import com.volmit.iris.core.gui.components.MapLabels;
import com.volmit.iris.core.gui.components.MapTile;
import com.volmit.iris.core.gui.components.RenderType;
import java.awt.EventQueue;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
/**
* Renders the map as tiles on fixed zoom levels, so zooming within a level and panning reuse tiles that are already drawn.
* Tiles keep rendering while the view moves, as long as they are still on screen.
*/
final class VisionRenderController implements AutoCloseable {
static final int TILE_PIXELS = 64;
static final double MINIMUM_VIEW_BLOCKS_PER_PIXEL = 1D / 8D;
static final double MAXIMUM_BLOCKS_PER_PIXEL = 4_096D;
private static final double MINIMUM_RENDER_BLOCKS_PER_PIXEL = 1D;
private static final int MAXIMUM_VISIBLE_TILES = 32_768;
private static final int PROBE_QUEUE_CAPACITY = 1;
private static final int COARSER_FALLBACK_LEVELS = 6;
private static final long CACHE_BYTES = 256L * 1024L * 1024L;
private final Runnable listener;
private final int workerCount;
private final ThreadPoolExecutor renderExecutor;
private final ThreadPoolExecutor probeExecutor;
private final WeightedTileCache cache;
private final Set<TileKey> tilesInFlight = new HashSet<>();
private final AtomicInteger activeWorkers = new AtomicInteger();
private final AtomicLong viewSequence = new AtomicLong();
private final AtomicLong probeSequence = new AtomicLong();
private final AtomicBoolean closed = new AtomicBoolean();
private final AtomicBoolean publicationQueued = new AtomicBoolean();
private final AtomicBoolean publicationDirty = new AtomicBoolean();
private volatile Frame currentFrame;
VisionRenderController(Runnable listener) {
this.listener = Objects.requireNonNull(listener, "listener");
int processors = Math.max(1, Runtime.getRuntime().availableProcessors());
this.workerCount = Math.max(1, Math.min(6, processors / 2));
AtomicInteger threadSequence = new AtomicInteger();
this.renderExecutor = new ThreadPoolExecutor(
workerCount,
workerCount,
30L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
daemonFactory("Iris Vision Render", Thread.NORM_PRIORITY, threadSequence)
);
this.renderExecutor.allowCoreThreadTimeOut(true);
this.probeExecutor = new ThreadPoolExecutor(
1,
1,
0L,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(PROBE_QUEUE_CAPACITY),
daemonFactory("Iris Vision Probe", Thread.MIN_PRIORITY, threadSequence),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
this.cache = new WeightedTileCache(CACHE_BYTES);
GuiHost.interruptOnPluginShutdown(renderExecutor);
GuiHost.interruptOnPluginShutdown(probeExecutor);
}
// Snapping to powers of two keeps every zoom within a level on the same tiles; nearest keeps tiles within 1.41x of screen size.
static double renderBlocksPerPixel(double viewBlocksPerPixel) {
double level = Math.pow(2D, Math.round(Math.log(viewBlocksPerPixel) / Math.log(2D)));
return Math.max(MINIMUM_RENDER_BLOCKS_PER_PIXEL, Math.min(MAXIMUM_BLOCKS_PER_PIXEL, level));
}
Frame request(RenderSpec spec) {
Objects.requireNonNull(spec, "spec");
if (closed.get()) {
throw new IllegalStateException("Vision render controller is closed");
}
double renderScale = renderBlocksPerPixel(spec.blocksPerPixel());
List<VisibleTile> tiles = visibleTiles(spec, renderScale);
Frame frame = new Frame(viewSequence.incrementAndGet(), spec, renderScale, tiles);
for (VisibleTile tile : tiles) {
tile.setTile(cachedTileServing(tile.key()));
}
currentFrame = frame;
probeSequence.incrementAndGet();
probeExecutor.getQueue().clear();
startWorkers();
publish();
return frame;
}
/**
* A tile from another zoom level covering the given one, to draw while the real tile renders.
*/
Fallback fallback(Frame frame, VisibleTile tile) {
MapTile withoutHeights = cache.get(tile.key().withHeights(false));
if (withoutHeights != null) {
return new Fallback(List.of(new FallbackPiece(withoutHeights, 0, 0, TILE_PIXELS, 0D, 0D, 1D)));
}
Fallback coarser = coarserFallback(frame, tile);
return coarser != null ? coarser : finerFallback(frame, tile);
}
// A tile drawn with heights serves just as well where no heights are needed, so turning shading off redraws nothing.
private MapTile cachedTileServing(TileKey key) {
MapTile exact = cache.get(key);
if (exact != null || key.heights()) {
return exact;
}
return cache.get(key.withHeights(true));
}
Progress progress(Frame frame) {
if (frame == null) {
return new Progress(0, 0, 0);
}
int ready = 0;
for (VisibleTile tile : frame.tiles()) {
if (tile.tile() != null) {
ready++;
}
}
return new Progress(frame.tiles().size(), ready, activeWorkers.get());
}
<T> void submitProbe(Frame frame, Callable<T> probe, Consumer<T> consumer) {
Objects.requireNonNull(probe, "probe");
Objects.requireNonNull(consumer, "consumer");
if (frame == null || closed.get() || currentFrame != frame) {
return;
}
long probeRevision = probeSequence.incrementAndGet();
probeExecutor.getQueue().clear();
try {
probeExecutor.execute(() -> runProbe(probeRevision, probe, consumer));
} catch (RejectedExecutionException ignored) {
probeExecutor.getQueue().clear();
}
}
@Override
public void close() {
if (!closed.compareAndSet(false, true)) {
return;
}
currentFrame = null;
renderExecutor.shutdownNow();
probeExecutor.shutdownNow();
cache.clear();
}
static List<VisibleTile> visibleTiles(RenderSpec spec, double renderScale) {
Objects.requireNonNull(spec, "spec");
double blocksPerPixel = spec.blocksPerPixel();
double tileSpan = TILE_PIXELS * renderScale;
double halfWidth = spec.width() * blocksPerPixel * 0.5D;
double halfHeight = spec.height() * blocksPerPixel * 0.5D;
long minimumX = floorTile(spec.centerX() - halfWidth, tileSpan);
long maximumX = floorTile(Math.nextDown(spec.centerX() + halfWidth), tileSpan);
long minimumZ = floorTile(spec.centerZ() - halfHeight, tileSpan);
long maximumZ = floorTile(Math.nextDown(spec.centerZ() + halfHeight), tileSpan);
long tileCount = Math.multiplyExact(maximumX - minimumX + 1L, maximumZ - minimumZ + 1L);
if (tileCount > MAXIMUM_VISIBLE_TILES) {
throw new IllegalArgumentException("Vision viewport contains too many tiles");
}
ArrayList<VisibleTile> tiles = new ArrayList<>((int) tileCount);
double centerTileX = spec.centerX() / tileSpan;
double centerTileZ = spec.centerZ() / tileSpan;
for (long tileZ = minimumZ; tileZ <= maximumZ; tileZ++) {
for (long tileX = minimumX; tileX <= maximumX; tileX++) {
double deltaX = tileX + 0.5D - centerTileX;
double deltaZ = tileZ + 0.5D - centerTileZ;
TileKey key = new TileKey(spec.contentRevision(), spec.type(), spec.heights(), renderScale, tileX, tileZ);
tiles.add(new VisibleTile(key, deltaX * deltaX + deltaZ * deltaZ));
}
}
tiles.sort(Comparator.comparingDouble(VisibleTile::distanceSquared)
.thenComparingLong(VisibleTile::tileZ)
.thenComparingLong(VisibleTile::tileX));
return List.copyOf(tiles);
}
private static long floorTile(double coordinate, double tileSpan) {
double tile = Math.floor(coordinate / tileSpan);
if (tile < Long.MIN_VALUE || tile > Long.MAX_VALUE) {
throw new IllegalArgumentException("Vision viewport exceeds tile coordinate range");
}
return (long) tile;
}
private static ThreadFactory daemonFactory(String name, int priority, AtomicInteger sequence) {
return (Runnable runnable) -> {
Thread thread = new Thread(runnable, name + " " + sequence.incrementAndGet());
thread.setDaemon(true);
thread.setPriority(priority);
thread.setUncaughtExceptionHandler((Thread failedThread, Throwable error) -> Iris.reportError(error));
return thread;
};
}
private void startWorkers() {
while (!closed.get()) {
int active = activeWorkers.get();
if (active >= workerCount || !hasWork()) {
return;
}
if (!activeWorkers.compareAndSet(active, active + 1)) {
continue;
}
try {
renderExecutor.execute(this::renderUntilIdle);
} catch (RejectedExecutionException ignored) {
activeWorkers.decrementAndGet();
return;
}
}
}
// Each worker keeps taking the most central missing tile of whatever frame is current, until the screen is covered.
private void renderUntilIdle() {
try {
TileJob job;
while ((job = claimNextTile()) != null) {
render(job);
}
} finally {
activeWorkers.decrementAndGet();
}
startWorkers();
}
private synchronized boolean hasWork() {
Frame frame = currentFrame;
return frame != null && frame.nextMissing(tilesInFlight, false) != null;
}
private synchronized TileJob claimNextTile() {
Frame frame = currentFrame;
if (frame == null || closed.get()) {
return null;
}
VisibleTile tile = frame.nextMissing(tilesInFlight, true);
if (tile == null) {
return null;
}
tilesInFlight.add(tile.key());
return new TileJob(frame.spec().renderer(), tile.key());
}
private synchronized void release(TileKey key) {
tilesInFlight.remove(key);
}
private void render(TileJob job) {
TileKey key = job.key();
try {
double tileSpan = TILE_PIXELS * key.blocksPerPixel();
MapTile tile = job.renderer().renderTile(
key.tileX() * tileSpan,
key.tileZ() * tileSpan,
key.blocksPerPixel(),
TILE_PIXELS,
key.type(),
key.heights(),
() -> !isWanted(key)
);
cache.put(key, tile);
Frame frame = currentFrame;
VisibleTile visible = frame == null ? null : frame.tile(key);
if (visible != null) {
visible.setTile(tile);
publish();
}
} catch (CancellationException ignored) {
} catch (Throwable error) {
Iris.debug("Vision tile render failed: " + error.getClass().getSimpleName() + ": " + error.getMessage());
} finally {
release(key);
}
}
private boolean isWanted(TileKey key) {
Frame frame = currentFrame;
return !closed.get() && frame != null && frame.tile(key) != null;
}
private Fallback coarserFallback(Frame frame, VisibleTile tile) {
TileKey key = tile.key();
for (int level = 1; level <= COARSER_FALLBACK_LEVELS; level++) {
long scale = 1L << level;
int sourceSize = TILE_PIXELS >> level;
double parentScale = key.blocksPerPixel() * scale;
if (sourceSize < 1 || parentScale > MAXIMUM_BLOCKS_PER_PIXEL) {
return null;
}
long parentX = Math.floorDiv(key.tileX(), scale);
long parentZ = Math.floorDiv(key.tileZ(), scale);
MapTile parent = cache.get(new TileKey(key.contentRevision(), key.type(), key.heights(), parentScale, parentX,
parentZ));
if (parent != null) {
int sourceX = (int) (key.tileX() - parentX * scale) * sourceSize;
int sourceZ = (int) (key.tileZ() - parentZ * scale) * sourceSize;
return new Fallback(List.of(new FallbackPiece(parent, sourceX, sourceZ, sourceSize, 0D, 0D, 1D)));
}
}
return null;
}
private Fallback finerFallback(Frame frame, VisibleTile tile) {
TileKey key = tile.key();
double childScale = key.blocksPerPixel() / 2D;
if (childScale < MINIMUM_RENDER_BLOCKS_PER_PIXEL) {
return null;
}
List<FallbackPiece> pieces = new ArrayList<>(4);
for (int childZ = 0; childZ < 2; childZ++) {
for (int childX = 0; childX < 2; childX++) {
MapTile child = cache.get(new TileKey(key.contentRevision(), key.type(), key.heights(), childScale,
key.tileX() * 2L + childX, key.tileZ() * 2L + childZ));
if (child != null) {
pieces.add(new FallbackPiece(child, 0, 0, TILE_PIXELS, childX * 0.5D, childZ * 0.5D, 0.5D));
}
}
}
return pieces.isEmpty() ? null : new Fallback(List.copyOf(pieces));
}
private <T> void runProbe(long probeRevision, Callable<T> probe, Consumer<T> consumer) {
try {
if (!isProbeCurrent(probeRevision)) {
return;
}
T result = probe.call();
if (!isProbeCurrent(probeRevision)) {
return;
}
EventQueue.invokeLater(() -> {
if (isProbeCurrent(probeRevision)) {
consumer.accept(result);
}
});
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
} catch (Throwable error) {
Iris.debug("Vision probe failed: " + error.getClass().getSimpleName() + ": " + error.getMessage());
}
}
private boolean isProbeCurrent(long probeRevision) {
return !closed.get() && probeSequence.get() == probeRevision;
}
private void publish() {
if (closed.get()) {
return;
}
publicationDirty.set(true);
queuePublication();
}
private void queuePublication() {
if (!publicationQueued.compareAndSet(false, true)) {
return;
}
EventQueue.invokeLater(() -> {
try {
if (publicationDirty.getAndSet(false) && !closed.get()) {
listener.run();
}
} finally {
publicationQueued.set(false);
if (publicationDirty.get() && !closed.get()) {
queuePublication();
}
}
});
}
record RenderSpec(
IrisRenderer renderer,
RenderType type,
long contentRevision,
double centerX,
double centerZ,
double blocksPerPixel,
int width,
int height,
boolean heights
) {
RenderSpec {
Objects.requireNonNull(renderer, "renderer");
Objects.requireNonNull(type, "type");
if (!Double.isFinite(centerX) || !Double.isFinite(centerZ)) {
throw new IllegalArgumentException("Vision center must be finite");
}
if (!Double.isFinite(blocksPerPixel)
|| blocksPerPixel < MINIMUM_VIEW_BLOCKS_PER_PIXEL
|| blocksPerPixel > MAXIMUM_BLOCKS_PER_PIXEL) {
throw new IllegalArgumentException("Vision scale is outside the supported range");
}
if (width < 1 || height < 1) {
throw new IllegalArgumentException("Vision viewport must be positive");
}
}
}
static final class Frame {
private final long viewRevision;
private final RenderSpec spec;
private final double renderBlocksPerPixel;
private final List<VisibleTile> tiles;
private final Map<TileKey, VisibleTile> tilesByKey;
private int nextCandidate;
private Frame(long viewRevision, RenderSpec spec, double renderBlocksPerPixel, List<VisibleTile> tiles) {
this.viewRevision = viewRevision;
this.spec = Objects.requireNonNull(spec, "spec");
this.renderBlocksPerPixel = renderBlocksPerPixel;
this.tiles = tiles;
this.tilesByKey = new HashMap<>(tiles.size() * 2);
for (VisibleTile tile : tiles) {
tilesByKey.put(tile.key(), tile);
}
}
long viewRevision() {
return viewRevision;
}
RenderSpec spec() {
return spec;
}
double renderBlocksPerPixel() {
return renderBlocksPerPixel;
}
List<VisibleTile> tiles() {
return tiles;
}
private VisibleTile tile(TileKey key) {
return tilesByKey.get(key);
}
/**
* The tile covering a world position together with the pixel of it there, or null when that tile is not drawn yet.
*/
TilePixel pixelAt(double worldX, double worldZ) {
double tileSpan = TILE_PIXELS * renderBlocksPerPixel;
long tileX = (long) Math.floor(worldX / tileSpan);
long tileZ = (long) Math.floor(worldZ / tileSpan);
VisibleTile visible = tilesByKey.get(new TileKey(spec.contentRevision(), spec.type(), spec.heights(),
renderBlocksPerPixel, tileX, tileZ));
MapTile tile = visible == null ? null : visible.tile();
if (tile == null) {
return null;
}
int pixelX = pixelWithin(worldX - tileX * tileSpan);
int pixelZ = pixelWithin(worldZ - tileZ * tileSpan);
return new TilePixel(tile, pixelX, pixelZ);
}
private int pixelWithin(double offsetBlocks) {
return Math.max(0, Math.min(TILE_PIXELS - 1, (int) Math.floor(offsetBlocks / renderBlocksPerPixel)));
}
// Tiles are sorted centre first, and everything before the cursor is drawn or claimed, so each search resumes there.
private VisibleTile nextMissing(Set<TileKey> inFlight, boolean advance) {
for (int index = nextCandidate; index < tiles.size(); index++) {
VisibleTile tile = tiles.get(index);
if (tile.tile() == null && !inFlight.contains(tile.key())) {
if (advance) {
nextCandidate = index + 1;
}
return tile;
}
}
if (advance) {
nextCandidate = tiles.size();
}
return null;
}
}
static final class VisibleTile {
private final TileKey key;
private final double distanceSquared;
private volatile MapTile tile;
private VisibleTile(TileKey key, double distanceSquared) {
this.key = key;
this.distanceSquared = distanceSquared;
}
TileKey key() {
return key;
}
long tileX() {
return key.tileX();
}
long tileZ() {
return key.tileZ();
}
double distanceSquared() {
return distanceSquared;
}
MapTile tile() {
return tile;
}
private void setTile(MapTile tile) {
this.tile = tile;
}
}
/**
* @param heights whether the tile also carries terrain heights, for shading
*/
record TileKey(long contentRevision, RenderType type, boolean heights, double blocksPerPixel, long tileX, long tileZ) {
TileKey {
Objects.requireNonNull(type, "type");
if (!Double.isFinite(blocksPerPixel) || blocksPerPixel <= 0D) {
throw new IllegalArgumentException("Vision cache scale must be finite and positive");
}
}
TileKey withHeights(boolean nextHeights) {
return new TileKey(contentRevision, type, nextHeights, blocksPerPixel, tileX, tileZ);
}
}
record TilePixel(MapTile tile, int pixelX, int pixelZ) {
MapLabels.Label subject() {
return tile.labels().label(tile.subjectAt(pixelX, pixelZ));
}
MapLabels.Label region() {
return tile.labels().label(tile.regionAt(pixelX, pixelZ));
}
}
/**
* Pieces of cached tiles that together cover a missing tile; offsets and size are fractions of the tile.
*/
record Fallback(List<FallbackPiece> pieces) {
}
record FallbackPiece(MapTile tile, int sourceX, int sourceZ, int sourceSize, double offsetX, double offsetZ,
double size) {
}
record Progress(int total, int ready, int activeWorkers) {
double completion() {
if (total == 0) {
return 1D;
}
return Math.min(1D, ready / (double) total);
}
}
private record TileJob(IrisRenderer renderer, TileKey key) {
}
private static final class WeightedTileCache {
private final long maximumBytes;
private final LinkedHashMap<TileKey, CacheEntry> entries;
private long bytes;
private WeightedTileCache(long maximumBytes) {
this.maximumBytes = maximumBytes;
this.entries = new LinkedHashMap<>(128, 0.75F, true);
}
synchronized MapTile get(TileKey key) {
CacheEntry entry = entries.get(key);
return entry == null ? null : entry.tile();
}
synchronized void put(TileKey key, MapTile tile) {
long tileBytes = tile.bytes();
CacheEntry previous = entries.put(key, new CacheEntry(tile, tileBytes));
if (previous != null) {
bytes -= previous.bytes();
}
bytes += tileBytes;
while (bytes > maximumBytes && !entries.isEmpty()) {
Iterator<Map.Entry<TileKey, CacheEntry>> iterator = entries.entrySet().iterator();
Map.Entry<TileKey, CacheEntry> eldest = iterator.next();
bytes -= eldest.getValue().bytes();
iterator.remove();
}
}
synchronized void clear() {
entries.clear();
bytes = 0L;
}
}
private record CacheEntry(MapTile tile, long bytes) {
}
}
@@ -0,0 +1,60 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
record VisionViewport(double centerX, double centerZ, double blocksPerPixel) {
VisionViewport {
if (!Double.isFinite(centerX) || !Double.isFinite(centerZ)) {
throw new IllegalArgumentException("Vision viewport center must be finite");
}
if (!Double.isFinite(blocksPerPixel) || blocksPerPixel <= 0D) {
throw new IllegalArgumentException("Vision viewport scale must be finite and positive");
}
}
double worldX(double screenX, int width) {
return centerX + (screenX - width * 0.5D) * blocksPerPixel;
}
double worldZ(double screenZ, int height) {
return centerZ + (screenZ - height * 0.5D) * blocksPerPixel;
}
VisionViewport zoomAt(
double screenX,
double screenZ,
int width,
int height,
double factor,
double minimumScale,
double maximumScale
) {
if (!Double.isFinite(factor) || factor <= 0D) {
throw new IllegalArgumentException("Vision zoom factor must be finite and positive");
}
double anchorX = worldX(screenX, width);
double anchorZ = worldZ(screenZ, height);
double nextScale = Math.max(minimumScale, Math.min(maximumScale, blocksPerPixel * factor));
return new VisionViewport(
anchorX - (screenX - width * 0.5D) * nextScale,
anchorZ - (screenZ - height * 0.5D) * nextScale,
nextScale
);
}
}
@@ -0,0 +1,86 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.volmit.iris.core.gui;
import com.sun.jna.platform.linux.LibC;
import com.volmit.iris.Iris;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Java does not recognise the non-reparenting Xwayland window managers of compositors such as niri, sway and Hyprland.
* It waits for a reparent that never comes and paints only a corner of each window. Java reads this hint from the
* native environment once, at the first decorated window, so it is set through libc before Iris opens any window.
*/
final class XwaylandWindowManagerHint {
private static final String NON_REPARENTING_VARIABLE = "_JAVA_AWT_WM_NONREPARENTING";
private static final int KEEP_EXISTING_VALUE = 0;
private static final List<String> REPARENTING_DESKTOPS = List.of("GNOME", "KDE");
private static final AtomicBoolean APPLIED = new AtomicBoolean();
private XwaylandWindowManagerHint() {
}
static void apply() {
if (!APPLIED.compareAndSet(false, true) || !isNeeded(System.getenv(), System.getProperty("os.name", ""))) {
return;
}
try {
if (LibC.INSTANCE.setenv(NON_REPARENTING_VARIABLE, "1", KEEP_EXISTING_VALUE) != 0) {
warnUnapplied();
}
} catch (Throwable e) {
Iris.reportError(e);
warnUnapplied();
}
}
static boolean isNeeded(Map<String, String> environment, String osName) {
return osName.toLowerCase(Locale.ROOT).contains("linux")
&& isSet(environment, "WAYLAND_DISPLAY")
&& isSet(environment, "DISPLAY")
&& !isSet(environment, NON_REPARENTING_VARIABLE)
&& !isReparentingDesktop(environment.get("XDG_CURRENT_DESKTOP"));
}
private static boolean isReparentingDesktop(String currentDesktop) {
if (currentDesktop == null) {
return false;
}
return Arrays.stream(currentDesktop.split(":"))
.map(desktop -> desktop.trim().toUpperCase(Locale.ROOT))
.anyMatch(REPARENTING_DESKTOPS::contains);
}
private static boolean isSet(Map<String, String> environment, String variable) {
String value = environment.get(variable);
return value != null && !value.isBlank();
}
private static void warnUnapplied() {
Iris.warn("Iris could not adjust Java for this Wayland desktop, so its windows may stay blank. "
+ "Start the server with %s=1 set to fix that.", NON_REPARENTING_VARIABLE);
}
}

Some files were not shown because too many files have changed in this diff Show More