Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
e2fd57d
Implement ReplaceBlocks UI source modes and preview
yyty947 Jun 26, 2026
ee69cae
Add per-rule ReplaceBlocks preview counts
yyty947 Jun 26, 2026
e65715b
Auto completion for builder ui
yyty947 Jun 26, 2026
f91b4a6
Update docs
yyty947 Jun 28, 2026
8adfa30
Complete ReplaceBlocks tile safety controls
yyty947 Jun 28, 2026
8cf71ea
Polish ReplaceBlocks builder input UX
yyty947 Jun 28, 2026
90604de
Merge branch 'master' into feature/replace-blocks-ui
yyty947 Jul 7, 2026
ea2141d
Implement Phase 4F-1
yyty947 Jul 7, 2026
71ba781
Fix ReplaceBlocks builder dropdown UX
yyty947 Jul 7, 2026
cc892a6
Fix ReplaceBlocks property dropdown interactions
yyty947 Jul 8, 2026
7b253b6
Implement ReplaceBlocks biome restrictions
yyty947 Jul 8, 2026
0302cf9
Add biome suggestions to ReplaceBlocks builder
yyty947 Jul 8, 2026
a31fe26
Implement ReplaceBlocks presets
yyty947 Jul 8, 2026
b41d8f0
Polish ReplaceBlocks builder presets
yyty947 Jul 8, 2026
cc4c3bb
Refine ReplaceBlocks preset saving
yyty947 Jul 8, 2026
a1864f0
Align ReplaceBlocks builder inputs
yyty947 Jul 8, 2026
24d73af
Fix ReplaceBlocks builder rule editing
yyty947 Jul 8, 2026
3526127
Harden ReplaceBlocks release paths
yyty947 Jul 10, 2026
d17f024
Record Phase 6 release evidence
yyty947 Jul 11, 2026
327f0c0
Fix ReplaceBlocks release regressions
yyty947 Jul 11, 2026
13f264b
Fix final ReplaceBlocks UI and relight regressions
yyty947 Jul 12, 2026
acaaa89
Update build.gradle
yyty947 Jul 12, 2026
a7c0870
Complete ReplaceBlocks translations
yyty947 Jul 12, 2026
fbdab21
Add Builder shortcuts design
yyty947 Jul 12, 2026
6d1f7df
Improve Builder shortcuts and rule selection
yyty947 Jul 12, 2026
671a798
Fix Builder shortcut routing
yyty947 Jul 12, 2026
ad4875c
Fix Builder autocomplete and marquee drag
yyty947 Jul 12, 2026
a095063
Fix Builder autocomplete key capture
yyty947 Jul 12, 2026
71eeda6
Improve Builder dropdown navigation
yyty947 Jul 12, 2026
12ca917
Clear stale Builder popup selection
yyty947 Jul 13, 2026
54f640a
Fix ReplaceBlocks biome palette decoding
yyty947 Jul 13, 2026
f1ca9f7
Fail closed for unknown synthetic biomes
yyty947 Jul 13, 2026
88964c2
Fix ReplaceBlocks duplicate source semantics
yyty947 Jul 13, 2026
92e5901
Scope ReplaceBlocks preview cancellation
yyty947 Jul 13, 2026
25ad13b
Avoid ReplaceBlocks writes without matches
yyty947 Jul 13, 2026
45122d5
Relight only saved ReplaceBlocks neighbors
yyty947 Jul 13, 2026
be60890
Disclose ReplaceBlocks relight side effects
yyty947 Jul 13, 2026
bb4d572
Fix initial Builder navigation and marquee layout
yyty947 Jul 14, 2026
6435b34
Fix Builder marquee layering
yyty947 Jul 14, 2026
6090b3b
Fix Builder first-popup anchoring
yyty947 Jul 14, 2026
29fdfca
Close ReplaceBlocks Phase 6
yyty947 Jul 14, 2026
2ac9869
Harden ReplaceBlocks parsing boundaries
yyty947 Jul 14, 2026
dd3272c
Share ReplaceBlocks parser core
yyty947 Jul 14, 2026
bd451d1
Make block-state catalogs extensible
yyty947 Jul 14, 2026
c1ba0b5
Merge remote-tracking branch 'upstream/master' into feature/replace-b…
yyty947 Jul 17, 2026
e062602
Complete ReplaceBlocks B-class hardening
yyty947 Jul 17, 2026
beddfd7
Document Builder catalogue switch reset
yyty947 Jul 17, 2026
b69fda1
Add versioned Builder catalogues
yyty947 Jul 17, 2026
4ef83f0
Guard ReplaceBlocks catalog switching
yyty947 Jul 17, 2026
75d4015
Fix catalog switch confirmation timing
yyty947 Jul 17, 2026
ef416b6
Warn on custom preset catalog mismatches
yyty947 Jul 17, 2026
910be04
Preserve legacy preset source semantics
yyty947 Jul 17, 2026
d91a0c7
Localize catalogue switch reset
yyty947 Jul 17, 2026
26c0709
Remove tracked Task 3 scratch report
yyty947 Jul 17, 2026
c58a23f
Record Builder catalogue switch acceptance
yyty947 Jul 17, 2026
233b4fc
Merge remote-tracking branch 'upstream/master' into feature/replace-b…
yyty947 Jul 18, 2026
63fda84
Polish ReplaceBlocks Builder layout
yyty947 Jul 18, 2026
1ed2c23
Keep Builder autocomplete popups inside dialog
yyty947 Jul 18, 2026
cd3fe0e
Improve uncaught exception logging
yyty947 Jul 25, 2026
a86e5c6
Optimize ReplaceBlocks Builder responsiveness
yyty947 Jul 25, 2026
3b735b1
Fix transactional Builder closing
yyty947 Jul 25, 2026
1e19f30
Refine ReplaceBlocks Builder guidance
yyty947 Jul 27, 2026
53ad50d
Localize ReplaceBlocks Builder workflow text
yyty947 Jul 27, 2026
dfd6bc8
Harden Builder release validation
yyty947 Jul 28, 2026
8954493
Make Builder checks platform-safe
yyty947 Jul 28, 2026
8bdb673
Stabilize Builder popups across JavaFX pulses
yyty947 Jul 28, 2026
a7fd8e2
Allow native popup direction in platform checks
yyty947 Jul 28, 2026
7e86f77
Diagnose macOS popup geometry
yyty947 Jul 28, 2026
9f82681
Use visual tolerance for popup attachment
yyty947 Jul 28, 2026
d708ca4
Capture macOS popup geometry
yyty947 Jul 28, 2026
388f528
Keep popup resize checks within screen bounds
yyty947 Jul 28, 2026
9c3527f
Capture popup attachment stage
yyty947 Jul 28, 2026
41d6a12
Attach Builder popups in either direction
yyty947 Jul 28, 2026
e729044
Update final ReplaceBlocks release review
yyty947 Jul 28, 2026
ba6c74d
Match Builder popup framing to menus
yyty947 Jul 28, 2026
244426d
Record final ReplaceBlocks UI acceptance
yyty947 Jul 28, 2026
292232e
Remove internal development artifacts from PR
yyty947 Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,4 @@ jobs:
uses: AButler/upload-release-assets@v3.0
with:
files: build/jpackage/*.${{ matrix.type }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@

The wiki can be found [here](https://github.com/Querz/mcaselector/wiki).

Fork documentation: [ReplaceBlocks Builder, syntax, compatibility, and safety](docs/REPLACE_BLOCKS.md).

## Video Tutorials
For people who prefer watching a video to understand how the MCA Selector works, there some very good tutorials on
YouTube explaining the basics:
Expand Down
5 changes: 3 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,9 @@ shadowJar {
)
from 'LICENSE'
}

// Allow a user-local JDK view to provide JavaFX jmods without modifying the installed JDK.
runtime {
javaHome = project.findProperty('runtime.jdkHome') ?: System.getenv('JAVA_HOME')
options = ['--strip-debug', '--compress', 'zip-6', '--no-header-files', '--no-man-pages']
modules = [
'java.base',
Expand Down Expand Up @@ -195,4 +196,4 @@ static minCss(i, o) throws IOException {
catch (Exception ex) {
ex.printStackTrace()
}
}
}
43 changes: 43 additions & 0 deletions docs/REPLACE_BLOCKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# ReplaceBlocks

ReplaceBlocks changes matching blocks in selected chunks. It is destructive: close Minecraft, make a backup, and test unfamiliar rules on a copied world first.

## Builder and advanced text

The Builder generates the same text format used by the existing advanced field. Builder rules can be edited as text, and valid advanced rules can be reopened in the Builder when their controls are representable. Advanced state or tile SNBT that has no dedicated control is preserved as fallback text.

Common source forms:

- `literal(minecraft:stone)` matches that block ID exactly.
- `regex(minecraft:.*_log)` intentionally uses a Java regular expression.
- `{Name:"minecraft:stone",Properties:{...}}` is an exact stored block-state match.
- `props({Name:"minecraft:oak_stairs",Properties:{facing:"north"}})` matches the named block and only the listed properties.
- `tile(source)` and `no_tile(source)` require or exclude a block entity at the source position.
- `y(-64..64, source)` restricts the source Y coordinate; either bound may be omitted.
- `biome(minecraft:plains;minecraft:forest, source)` restricts modern 1.18+ biome cells.

Targets may be a block ID, block-state SNBT, or a target followed by block-entity SNBT, for example `minecraft:barrel;{id:"minecraft:barrel"}`. Multiple rules are comma-separated.

Legacy bare or quoted sources remain Java regular expressions for compatibility. Prefer `literal(...)` or `regex(...)` in new rules so intent is explicit.

## Catalogue compatibility

The Builder bundles block-state catalogues for Java 1.18.2, 1.20.6, 1.21.9, 1.21.11, and 26.2. The newest catalogue is selected by default; selection remains manual. Catalogues supply suggestions and property values only, and the displayed Java/DataVersion label identifies the active catalogue.

An empty Builder switches catalogue immediately. A non-empty Builder asks first: Cancel keeps the old catalogue and all current work, while Confirm selects the new catalogue and fully resets fields, properties, Extra NBT, Y/biome filters, rules, selections, generated text, validation, and open popups.

A syntactically valid future or modded resource ID remains accepted even when it is absent from the active catalogue; the Builder shows a non-blocking warning and provides no property suggestions for it. Target warnings are stronger because an unsupported target ID can produce a world that Minecraft cannot load as intended.

Built-in and custom presets remain versionless ReplaceBlocks text. Switching catalogue clears the current preset selection and Builder content but never deletes saved presets. Applying a custom preset still appends valid rules; an exact source or target ID outside the active catalogue produces a non-blocking advisory, while regex sources are not guessed.

The catalogue is not world-version detection and does not guarantee that an ID exists in the target world. MCA Selector does not rename or migrate block IDs between Minecraft versions. Verify renamed/removed IDs yourself.

## Preview and execution safety

Preview is non-mutating. It reports affected chunks/sections, per-rule matches, overlaps, block-entity changes, unsupported chunks, and errors. Preview counts are the recommended check before execution, but they do not replace a backup.

Replacing air can complete missing sections and greatly increase world size. Replacing blocks that carry block entities can remove or rewrite their data. Modern execution invalidates affected lighting and heightmaps; existing adjacent chunks may also be marked for relighting.

Tile, Y, and biome conditions fail closed on unsupported pre-1.18 formats. If a ReplaceBlocks-only operation fails in any chunk, MCA Selector aborts saving that region and logs the failing chunk coordinate. Mixed legacy field changes preserve the historical per-chunk behavior.

After a risky change, load only a copied world in the intended Minecraft version, save/reload it, and inspect the game log for chunk, palette, block-entity, lighting, or heightmap errors.
1 change: 1 addition & 0 deletions src/main/java/net/querz/mcaselector/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public static void main(String[] args) throws ExecutionException, InterruptedExc

Logging.setLogDir(Config.BASE_LOG_DIR);
Logging.updateThreadContext();
Logging.installUncaughtExceptionHandler();
Logger LOGGER = LogManager.getLogger(Main.class);

VersionHandler.init();
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/net/querz/mcaselector/changer/Field.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,13 @@ public boolean parseNewValue(String s) {
public abstract void change(ChunkData root);

public abstract void force(ChunkData root);

public boolean applyWithResult(ChunkData root, boolean force) {
if (force) {
force(root);
} else {
change(root);
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,199 +5,88 @@
import net.querz.mcaselector.io.mca.ChunkData;
import net.querz.mcaselector.version.ChunkFilter;
import net.querz.mcaselector.version.VersionHandler;
import net.querz.mcaselector.version.mapping.registry.BlockRegistry;
import net.querz.nbt.CompoundTag;
import net.querz.nbt.io.snbt.ParseException;
import net.querz.nbt.io.snbt.SNBTParser;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

public class ReplaceBlocksField extends Field<Map<String, ChunkFilter.BlockReplaceData>> {
public class ReplaceBlocksField extends Field<Map<ChunkFilter.BlockReplaceSource, ChunkFilter.BlockReplaceData>> {

public ReplaceBlocksField() {
super(FieldType.REPLACE_BLOCKS);
}

@Override
public boolean parseNewValue(String s) {

// format: <from=to>[,<from=to>,...]
// from format: minecraft:<block-name>
// <block-name>
// '<custom-block-name-with-namespace>'
// to format: minecraft:<block-name>
// <block-name>
// '<custom-block-name-with-namespace>'
// <snbt-string-block-state>
// <to>;<snbt-string-tile-entity>

Map<String, ChunkFilter.BlockReplaceData> newValue = new HashMap<>();

String trimmed = s.trim();

while (!trimmed.isEmpty()) {
String[] fromTo = trimmed.split("=", 2);
if (fromTo.length != 2) {
return super.parseNewValue(s);
}

String from = fromTo[0].trim();
if (from.startsWith("'") && from.endsWith("'") && from.length() > 2) {
from = from.substring(1, from.length() - 1);
} else if (!from.startsWith("minecraft:")) {
from = "minecraft:" + from;
if (!BlockRegistry.isValidName(from)) {
return super.parseNewValue(s);
}
}

String to = fromTo[1].trim();
int read = 0;
CompoundTag toState = null;
String toName = null;
if (to.startsWith("{")) {
// block state
try {
SNBTParser parser = new SNBTParser(to);
toState = (CompoundTag) parser.parse(true);
read += parser.getReadChars() - 1;
} catch (ParseException ex) {
return super.parseNewValue(s);
}
} else if (to.startsWith("'")) {
// quoted
int i = 1;
while (i < to.length() && to.charAt(i) != '\'') {
i++;
}
toName = to.substring(1, Math.max(i, to.length()));
if (!toName.endsWith("'")) {
return super.parseNewValue(s);
}
toName = toName.substring(0, toName.length() - 1);
if (toName.isEmpty()) {
return super.parseNewValue(s);
}
read += i + 1;
} else {
// minecraft block
// read everything until , or ;
int i = 0;
while (i < to.length()) {
if (to.charAt(i) == ',' || to.charAt(i) == ';') {
break;
}
i++;
}
toName = to.substring(0, i);
if (!toName.startsWith("minecraft:")) {
toName = "minecraft:" + toName;
}
if (!BlockRegistry.isValidName(toName)) {
return super.parseNewValue(s);
}
read += i;
}

to = to.substring(read).trim();

CompoundTag toTile = null;
if (to.startsWith(";")) {
to = to.substring(1).trim();
if (to.isEmpty()) {
return super.parseNewValue(s);
}
try {
SNBTParser parser = new SNBTParser(to);
toTile = (CompoundTag) parser.parse(true);
int readTile = parser.getReadChars();
to = to.substring(readTile - 1);
} catch (ParseException ex) {
return super.parseNewValue(s);
}
}

ChunkFilter.BlockReplaceData data;
if (toName != null && toTile != null) {
data = new ChunkFilter.BlockReplaceData(toName, toTile);
} else if (toName != null) {
data = new ChunkFilter.BlockReplaceData(toName);
} else if (toState != null && toTile != null) {
data = new ChunkFilter.BlockReplaceData(toState, toTile);
} else if (toState != null) {
data = new ChunkFilter.BlockReplaceData(toState);
} else {
return super.parseNewValue(s);
}
newValue.put(from, data);

to = to.trim();

if (to.startsWith(",")) {
trimmed = to.substring(1).trim();
} else if (!to.isEmpty()) {
return super.parseNewValue(s);
} else {
break;
}
public boolean parseNewValue(String value) {
ReplaceBlocksParser.Result result = ReplaceBlocksParser.parse(value);
if (!(result instanceof ReplaceBlocksParser.Success success)) {
return super.parseNewValue(value);
}

if (newValue.isEmpty()) {
return super.parseNewValue(s);
Map<ChunkFilter.BlockReplaceSource, ChunkFilter.BlockReplaceData> replacements = new LinkedHashMap<>();
for (ReplaceBlocksParser.ParsedRule rule : success.rules()) {
replacements.put(rule.source(), rule.target());
}

setNewValue(newValue);
if (replacements.isEmpty()) {
return super.parseNewValue(value);
}
setNewValue(replacements);
return true;
}

@Override
public Map<String, ChunkFilter.BlockReplaceData> getOldValue(ChunkData data) {
public Map<ChunkFilter.BlockReplaceSource, ChunkFilter.BlockReplaceData> getOldValue(ChunkData data) {
return null;
}

@Override
public void change(ChunkData data) {
VersionHandler.getImpl(data, ChunkFilter.Blocks.class).replaceBlocks(data, getNewValue());
applyReplacement(data);
}

private boolean applyReplacement(ChunkData data) {
if (!VersionHandler.getImpl(data, ChunkFilter.Blocks.class).replaceBlocks(data, getNewValue())) {
return false;
}
VersionHandler.getImpl(data, ChunkFilter.LightPopulated.class).setLightPopulated(data, (byte) 0);
ChunkFilter.Heightmap heightmap = VersionHandler.getImpl(data, ChunkFilter.Heightmap.class);
heightmap.worldSurface(data);
heightmap.oceanFloor(data);
heightmap.motionBlocking(data);
heightmap.motionBlockingNoLeaves(data);
return true;
}

@Override
public void force(ChunkData data) {
change(data);
applyReplacement(data);
}

@Override
public boolean applyWithResult(ChunkData data, boolean force) {
return applyReplacement(data);
}

@Override
public String toString() {
return getType().toString() + " = \"" + valueToString() + "\"";
return getType().toString() + " = \"" + escapeString(valueToString()) + "\"";
}

@Override
public String valueToString() {
StringBuilder sb = new StringBuilder();
boolean first = true;
for (Map.Entry<String, ChunkFilter.BlockReplaceData> entry : getNewValue().entrySet()) {
for (Map.Entry<ChunkFilter.BlockReplaceSource, ChunkFilter.BlockReplaceData> entry : getNewValue().entrySet()) {
if (first) {
first = false;
} else {
sb.append(", ");
}
String from = entry.getKey();
if (!from.startsWith("minecraft:")) {
sb.append("'").append(from).append("'");
} else {
sb.append(entry.getKey());
}
sb.append(entry.getKey());
sb.append("=");
sb.append(escapeString(entry.getValue().toString()));
sb.append(entry.getValue());
}
return sb.toString();
}

private String escapeString(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"");
private String escapeString(String value) {
return value.replace("\\", "\\\\").replace("\"", "\\\"");
}
}
Loading