[Direct Paste] Servux placement options/sub-region overrides are ignored and entity transforms are incorrect #4

Open
opened 2026-08-27 11:59:58 +00:00 by stavapin · 1 comment

Summary

I found several fidelity issues in the current Servux Direct Paste implementation on LitematicaFolia 0.6.1+26.2.

The most visible problems are:

  1. Litematica's paste replace mode is ignored.

    • NONE does not work.
    • ALL does not work.
    • Direct Paste always behaves approximately like WITH_NON_AIR.
  2. Entities can be spawned at incorrect positions.

    • This is especially obvious for entities such as minecarts.
    • Region offsets appear to be applied incorrectly.
  3. Entity coordinates are transformed incorrectly when the placement is rotated.

    • Fractional X/Z coordinates are not rotated correctly.
  4. Entity orientation is not rotated with the schematic placement.

  5. Per-sub-region placement overrides appear to be ignored.

    • position
    • rotation
    • enabled/disabled state
    • likely mirror as well
  6. Other standard Servux paste parameters are sent by Litematica and supported by upstream Servux, but are currently not represented in PasteOptions or applied by PasteOperation, including at least:

    • ReplaceMode
    • PasteLayerBehavior
    • RenderLayerRange
  7. paste.maxBlocksPerChunkTask appears to be advisory only and is not actually used to yield large per-chunk block operations.


Environment

  • LitematicaFolia: 0.6.1+26.2
  • Minecraft: 26.2
  • Server: Purpur 26.2-2622
  • Servux bridge: enabled
  • Direct Paste: otherwise functional
  • Standard Litematica client on Minecraft 26.2

The schematic blocks themselves generally paste successfully. The issues below concern placement semantics and entity fidelity.


1. ReplaceMode is ignored

Reproduction

Test A: ALL

  1. Create/load a schematic that contains an air cavity.

  2. Place it over existing solid blocks.

  3. In Litematica set:

    Generic -> pasteReplaceBehavior = All

  4. Paste using Servux Direct Paste.

Expected

Schematic air should also be pasted, removing existing destination blocks.

Actual

Existing blocks remain wherever the schematic contains air.


Test B: NONE

  1. Put existing blocks inside the target area.

  2. Set:

    Generic -> pasteReplaceBehavior = None

  3. Direct Paste the schematic.

Expected

Blocks should only be placed where the destination world currently contains air.

Actual

Non-air schematic blocks overwrite existing destination blocks.


Test C: WITH_NON_AIR

This is the only mode that currently behaves as expected:

  • schematic air is ignored
  • schematic non-air overwrites destination blocks

Client / Servux protocol side

This does not appear to be a client-side omission.

Litematica serializes the current paste options into the placement NBT, including:

compound.putString(
    "ReplaceMode",
    Configs.Generic.PASTE_REPLACE_BEHAVIOR.getStringValue()
);

compound.putString(
    "PasteLayerBehavior",
    Configs.Generic.PASTE_LAYER_BEHAVIOR.getStringValue()
);

compound.put(
    "RenderLayerRange",
    LayerRange.CODEC,
    DataManager.getRenderLayerRange()
);

The inline Direct Paste path then sends that same placement NBT with:

nbt.putString("Task", "LitematicaPaste");

ServuxLitematicaHandler.getInstance()
    .encodeClientData(
        ServuxLitematicaPacket.ResponseC2SStart(nbt)
    );

For oversized/transmitted schematics, the schematic payload itself is removed and streamed separately, while placement metadata is retained.

Upstream Servux 26.2 explicitly parses the same values in LitematicsDataProvider:

ReplaceBehavior replaceMode =
    ReplaceBehavior.fromStringStatic(
        tags.getStringOrDefault(
            "ReplaceMode",
            ReplaceBehavior.NONE.name()
        )
    );

PasteLayerBehavior layerBehavior =
    PasteLayerBehavior.fromStringStatic(
        tags.getStringOrDefault(
            "PasteLayerBehavior",
            PasteLayerBehavior.ALL.name()
        )
    );

LayerRange layerRange =
    tags.getCodec("RenderLayerRange", LayerRange.CODEC)
        .orElse(null);

So ReplaceMode is already part of the existing Litematica <-> Servux paste protocol.


LitematicaFolia cause

In:

src/main/java/fr/ekaii/litematica/protocol/handler/DirectPasteHandler.java

the Direct Paste implementation reads things such as:

Origin
IgnoreEntities
Rotation
Schematics

and then constructs a simplified PasteOptions.

Conceptually, it currently ends up similar to:

PasteOptions opts = new PasteOptions(
    origin,
    placeEntities,
    defaults.placeTileEntities(),
    defaults.placePendingTicks(),
    defaults.deferredPhysics(),
    defaults.observersLast(),
    defaults.maxBlocksPerChunkTask(),
    yaw,
    null
);

ReplaceMode is not represented.

PasteOptions itself currently has no replace-behavior field:

public record PasteOptions(
    Location origin,
    boolean placeEntities,
    boolean placeTileEntities,
    boolean placePendingTicks,
    boolean deferredPhysics,
    boolean observersLast,
    int maxBlocksPerChunkTask,
    int yawRotation,
    BiConsumer<Player, String> progress
)

Finally, in:

src/main/java/fr/ekaii/litematica/paste/PasteOperation.java

schematic air is unconditionally discarded:

if (paletteAir[paletteIdx]) continue;

while destination blocks are later overwritten:

Block b = world.getBlockAt(pw.x, pw.y, pw.z);
b.setBlockData(pw.data, !options.deferredPhysics());

There is no destination-air check.

This effectively hard-codes behavior equivalent to WITH_NON_AIR.


Possible fix

Add a local equivalent of Servux/Litematica's replace behavior to PasteOptions, for example:

public enum ReplaceBehavior {
    NONE,
    WITH_NON_AIR,
    ALL
}

and:

public record PasteOptions(
    Location origin,
    boolean placeEntities,
    boolean placeTileEntities,
    boolean placePendingTicks,
    boolean deferredPhysics,
    boolean observersLast,
    int maxBlocksPerChunkTask,
    int yawRotation,
    ReplaceBehavior replaceBehavior,
    BiConsumer<Player, String> progress
) {}

Parse the standard protocol field:

ReplaceMode

for both:

  • inline LitematicaPaste
  • transmitted/sliced Direct Paste

Then implement the three upstream semantics.

WITH_NON_AIR

Skip schematic air:

if (schematicAir) {
    continue;
}

ALL

Do not discard schematic air.

Air should become a normal planned write so that existing destination blocks can be removed.

NONE

Before writing a schematic non-air block:

Block destination = world.getBlockAt(pw.x, pw.y, pw.z);

if (!destination.getType().isAir()) {
    continue;
}

Ideally the exact behavior should follow upstream Servux/Litematica's ReplaceBehavior implementation rather than introducing different semantics.


2. Entity positions are offset incorrectly

This is reproducible with entities such as minecarts.

Blocks land at the correct placement coordinates, while entities may appear offset from the schematic.

Suspected source-level cause

In PasteOperation.planRegion() the entity coordinates are effectively handled as:

double lx = doubleOf(posList.get(0)) - region.originX;
double ly = doubleOf(posList.get(1)) - region.originY;
double lz = doubleOf(posList.get(2)) - region.originZ;

However, Litematica stores entity Pos values relative to the region origin when saving a schematic.

The client-side save logic effectively does:

Vec3d relPos = new Vec3d(
    entityX - regionOriginX,
    entityY - regionOriginY,
    entityZ - regionOriginZ
);

Therefore the values in the entity Pos list are already region-local.

LitematicaFolia later calculates the region world start using the region position again:

int[] rotRO = rotateXZ(region.originX, region.originZ, yaw);

int startX = origin.getBlockX() + rotRO[0];
int startY = origin.getBlockY() + region.originY;
int startZ = origin.getBlockZ() + rotRO[1];

Subtracting region.originX/Y/Z from entity Pos a second time therefore applies the region offset incorrectly.

For example:

region Position X = 100
entity local Pos X = 2.5

current:
lx = 2.5 - 100 = -97.5
startX = pasteOriginX + 100

final worldX ~= pasteOriginX + 2.5

Instead of:

worldX = pasteOriginX + 100 + 2.5

Possible fix

Do not subtract the region position again:

double lx = doubleOf(posList.get(0));
double ly = doubleOf(posList.get(1));
double lz = doubleOf(posList.get(2));

Then transform the region-local entity position using the same effective placement transform as the region blocks.

Once per-sub-region overrides are implemented, entity coordinates should use that same effective sub-region transform as well.


3. Fractional entity coordinates are rotated incorrectly

Current logic appears to rotate only the floored integer portion of the entity position and then re-add the original fractional components.

Conceptually:

int[] rxz = rotateXZdouble(lx, lz, yaw);

double wx = startX + rxz[0]
        + (lx - Math.floor(lx));

double wz = startZ + rxz[1]
        + (lz - Math.floor(lz));

with logic equivalent to:

private static int[] rotateXZdouble(double x, double z, int yaw) {
    return rotateXZ(
        (int) Math.floor(x),
        (int) Math.floor(z),
        yaw
    );
}

This means the integer coordinate is rotated, but the fractional X/Z components remain associated with their original axes.

Example:

(x, z) = (2.5, 3.25)

correct 90-degree transform:
(-3.25, 2.5)

The current integer-plus-original-fraction approach cannot produce the correct transformed coordinates.


Possible fix

Use a real double-coordinate transform:

private static double[] rotateXZDouble(double x, double z, int yaw) {
    return switch (yaw) {
        case 90  -> new double[] {-z,  x};
        case 180 -> new double[] {-x, -z};
        case 270 -> new double[] { z, -x};
        default  -> new double[] { x,  z};
    };
}

Then:

double[] rotated = rotateXZDouble(lx, lz, yaw);

double wx = startX + rotated[0];
double wy = startY + ly;
double wz = startZ + rotated[1];

The chunk key can still be calculated afterwards:

int cx = ((int) Math.floor(wx)) >> 4;
int cz = ((int) Math.floor(wz)) >> 4;

4. Entity yaw/orientation is not transformed

In the 26.2 NMS bridge, the entity is loaded from NBT and moved to the requested location while preserving its original yaw/pitch, conceptually:

entity.snapTo(
    loc.getX(),
    loc.getY(),
    loc.getZ(),
    entity.getYRot(),
    entity.getXRot()
);

If the whole schematic is rotated by 90/180/270 degrees, the blocks are rotated but the entity's own orientation is not adjusted.

This may affect:

  • minecarts
  • armor stands
  • display entities
  • mobs
  • other directional entities

Possible fix

Apply the effective placement transform to entity orientation before spawning.

For rotation-only behavior, conceptually:

float transformedYaw =
    normalizeYaw(entity.getYRot() + placementYaw);

then:

entity.snapTo(
    loc.getX(),
    loc.getY(),
    loc.getZ(),
    transformedYaw,
    entity.getXRot()
);

If global/sub-region mirroring is supported, entity orientation should use the same mirror + rotation transformation semantics as upstream Litematica/Servux instead of merely adding yaw.


5. SubRegions placement overrides appear to be ignored

This appears to be another important Servux compatibility issue.

The Direct Paste handler already recognizes that SubRegions contains per-region placement overrides rather than block data.

However, the effective sub-region placement does not appear to be reconstructed before PasteOperation runs.

The implementation primarily loads the raw schematic data from:

Schematics

and starts the paste operation.

This means a client placement that modifies an individual sub-region may still paste the original raw schematic-region layout.

Potentially affected settings include:

  • sub-region enabled/disabled state
  • sub-region position
  • sub-region rotation
  • sub-region mirror
  • possibly sub-region entity settings

Reproduction suggestion

Use a schematic with two sub-regions.

In the Litematica placement configuration:

  1. disable one region, or
  2. move one region, or
  3. rotate one region

Then Direct Paste the schematic.

Expected

The server-side paste should exactly match the placement visible on the client.

Suspected current behavior

The raw .litematic region arrangement may be pasted instead of the effective SubRegions placement state.


Possible fix

Instead of treating:

Schematics

as the complete placement definition, reconstruct an effective placement model from fields such as:

Origin
Rotation
Mirror
SubRegions
IgnoreEntities

The effective transformation should conceptually be:

world position
=
placement origin
+ global placement transform(sub-region position)
+ sub-region transform(local schematic coordinate)

Disabled sub-regions should not be planned at all.

Upstream Servux implementations such as:

SchematicPlacement.createFromData(...)
TaskPasteSchematicPerChunkDirect

may provide useful reference semantics.


6. Other standard Servux paste settings are not applied

Litematica sends standard placement state including at least:

ReplaceMode
PasteLayerBehavior
RenderLayerRange

and upstream Servux parses these fields.

LitematicaFolia currently has no corresponding representation for them in PasteOptions.

As a result, layer-limited pasting is also likely to differ from upstream Servux.

For example, if the user selects a limited paste/render layer range, Direct Paste may still paste the entire schematic.


Possible fix

Add the relevant Servux placement settings to a protocol-level placement model and filter planned blocks before populating the per-chunk write queues.

It may also be useful to audit the remaining upstream LitematicaPaste fields for parity, including where applicable:

ChangedBlocksOnly
IgnoreBlocks
Mirror

7. maxBlocksPerChunkTask appears to be unused

The config currently describes:

paste:
  # Hard cap on blocks placed per chunk-task before yielding back to the
  # region scheduler. Prevents long ticks on huge schematics.
  maxBlocksPerChunkTask: 8192

PasteOptions also exposes:

int maxBlocksPerChunkTask

but it appears to be advisory only.

The block application path iterates the entire per-chunk write collection in a single scheduled operation rather than using the configured limit to yield.

For a very large/dense chunk, this means all writes may still execute in one region scheduler task.


Possible fix

Split each per-chunk write list into batches of at most:

options.maxBlocksPerChunkTask()

and reschedule/yield the remaining work on the same owning region.

Care should be taken to preserve paste phases/order, for example:

pass 1 normal blocks
        ↓
tile entities
        ↓
pass 2 / active blocks
        ↓
entities + pending ticks
        ↓
physics/update sweep

Suggested architectural approach

Instead of continuing to add individual special cases to DirectPasteHandler, it may be cleaner to introduce a protocol-level placement description that mirrors the semantic data already provided by Litematica/Servux.

For example:

record ServuxPlacementOptions(
    Location origin,
    Rotation rotation,
    Mirror mirror,
    ReplaceBehavior replaceBehavior,
    PasteLayerBehavior pasteLayerBehavior,
    LayerRange layerRange,
    boolean ignoreEntities,
    Map<String, SubRegionOptions> subRegions
) {}

Then both Direct Paste transports:

inline LitematicaPaste
Litematic-Transmit / sliced paste

could decode into the same effective placement model before invoking PasteOperation.

PasteOperation would then be responsible only for applying an already-decoded placement.

This would also reduce behavior differences between the inline and sliced upload paths.


Upstream references

The expected protocol behavior can be compared against upstream Litematica and Servux.

Litematica

SchematicPlacement serializes placement state such as:

ReplaceMode
PasteLayerBehavior
RenderLayerRange
SubRegions

The Direct Paste path subsequently transmits this placement metadata to the server.

Servux 26.2

fi.dy.masa.servux.dataproviders.LitematicsDataProvider

parses fields including:

ReplaceMode
PasteLayerBehavior
RenderLayerRange
ChangedBlocksOnly
IgnoreBlocks
IgnoreEntities

and uses them when constructing the server-side paste task.

Because these semantics already exist in the upstream Litematica <-> Servux protocol, matching them in the bridge seems preferable to introducing plugin-specific interpretations.


Expected result

Servux Direct Paste through LitematicaFolia should reproduce the same effective placement that the Litematica client displays and that upstream Fabric + Servux would paste:

  • correct NONE / WITH_NON_AIR / ALL replace semantics
  • correct global placement rotation/mirror
  • correct sub-region enabled/position/rotation/mirror state
  • correct layer/range settings
  • correct entity world coordinates
  • correct fractional entity coordinate transforms
  • correct entity orientation
  • consistent behavior between inline and transmitted/sliced Direct Paste paths
  • actual yielding according to maxBlocksPerChunkTask

Thanks for the work on bringing native .litematic / Servux support to Paper/Folia.

The core Direct Paste transport itself is working well. These issues appear to be concentrated mainly in translating the full Litematica/Servux placement semantics into the current PasteOptions / PasteOperation pipeline.

## Summary I found several fidelity issues in the current Servux Direct Paste implementation on `LitematicaFolia 0.6.1+26.2`. The most visible problems are: 1. Litematica's paste replace mode is ignored. - `NONE` does not work. - `ALL` does not work. - Direct Paste always behaves approximately like `WITH_NON_AIR`. 2. Entities can be spawned at incorrect positions. - This is especially obvious for entities such as minecarts. - Region offsets appear to be applied incorrectly. 3. Entity coordinates are transformed incorrectly when the placement is rotated. - Fractional X/Z coordinates are not rotated correctly. 4. Entity orientation is not rotated with the schematic placement. 5. Per-sub-region placement overrides appear to be ignored. - position - rotation - enabled/disabled state - likely mirror as well 6. Other standard Servux paste parameters are sent by Litematica and supported by upstream Servux, but are currently not represented in `PasteOptions` or applied by `PasteOperation`, including at least: - `ReplaceMode` - `PasteLayerBehavior` - `RenderLayerRange` 7. `paste.maxBlocksPerChunkTask` appears to be advisory only and is not actually used to yield large per-chunk block operations. --- ## Environment - LitematicaFolia: `0.6.1+26.2` - Minecraft: `26.2` - Server: Purpur `26.2-2622` - Servux bridge: enabled - Direct Paste: otherwise functional - Standard Litematica client on Minecraft 26.2 The schematic blocks themselves generally paste successfully. The issues below concern placement semantics and entity fidelity. --- # 1. `ReplaceMode` is ignored ## Reproduction ### Test A: `ALL` 1. Create/load a schematic that contains an air cavity. 2. Place it over existing solid blocks. 3. In Litematica set: `Generic -> pasteReplaceBehavior = All` 4. Paste using Servux Direct Paste. ### Expected Schematic air should also be pasted, removing existing destination blocks. ### Actual Existing blocks remain wherever the schematic contains air. --- ### Test B: `NONE` 1. Put existing blocks inside the target area. 2. Set: `Generic -> pasteReplaceBehavior = None` 3. Direct Paste the schematic. ### Expected Blocks should only be placed where the destination world currently contains air. ### Actual Non-air schematic blocks overwrite existing destination blocks. --- ### Test C: `WITH_NON_AIR` This is the only mode that currently behaves as expected: - schematic air is ignored - schematic non-air overwrites destination blocks --- ## Client / Servux protocol side This does **not** appear to be a client-side omission. Litematica serializes the current paste options into the placement NBT, including: ```java compound.putString( "ReplaceMode", Configs.Generic.PASTE_REPLACE_BEHAVIOR.getStringValue() ); compound.putString( "PasteLayerBehavior", Configs.Generic.PASTE_LAYER_BEHAVIOR.getStringValue() ); compound.put( "RenderLayerRange", LayerRange.CODEC, DataManager.getRenderLayerRange() ); ``` The inline Direct Paste path then sends that same placement NBT with: ```java nbt.putString("Task", "LitematicaPaste"); ServuxLitematicaHandler.getInstance() .encodeClientData( ServuxLitematicaPacket.ResponseC2SStart(nbt) ); ``` For oversized/transmitted schematics, the schematic payload itself is removed and streamed separately, while placement metadata is retained. Upstream Servux 26.2 explicitly parses the same values in `LitematicsDataProvider`: ```java ReplaceBehavior replaceMode = ReplaceBehavior.fromStringStatic( tags.getStringOrDefault( "ReplaceMode", ReplaceBehavior.NONE.name() ) ); PasteLayerBehavior layerBehavior = PasteLayerBehavior.fromStringStatic( tags.getStringOrDefault( "PasteLayerBehavior", PasteLayerBehavior.ALL.name() ) ); LayerRange layerRange = tags.getCodec("RenderLayerRange", LayerRange.CODEC) .orElse(null); ``` So `ReplaceMode` is already part of the existing Litematica <-> Servux paste protocol. --- ## LitematicaFolia cause In: `src/main/java/fr/ekaii/litematica/protocol/handler/DirectPasteHandler.java` the Direct Paste implementation reads things such as: ```text Origin IgnoreEntities Rotation Schematics ``` and then constructs a simplified `PasteOptions`. Conceptually, it currently ends up similar to: ```java PasteOptions opts = new PasteOptions( origin, placeEntities, defaults.placeTileEntities(), defaults.placePendingTicks(), defaults.deferredPhysics(), defaults.observersLast(), defaults.maxBlocksPerChunkTask(), yaw, null ); ``` `ReplaceMode` is not represented. `PasteOptions` itself currently has no replace-behavior field: ```java public record PasteOptions( Location origin, boolean placeEntities, boolean placeTileEntities, boolean placePendingTicks, boolean deferredPhysics, boolean observersLast, int maxBlocksPerChunkTask, int yawRotation, BiConsumer<Player, String> progress ) ``` Finally, in: `src/main/java/fr/ekaii/litematica/paste/PasteOperation.java` schematic air is unconditionally discarded: ```java if (paletteAir[paletteIdx]) continue; ``` while destination blocks are later overwritten: ```java Block b = world.getBlockAt(pw.x, pw.y, pw.z); b.setBlockData(pw.data, !options.deferredPhysics()); ``` There is no destination-air check. This effectively hard-codes behavior equivalent to `WITH_NON_AIR`. --- ## Possible fix Add a local equivalent of Servux/Litematica's replace behavior to `PasteOptions`, for example: ```java public enum ReplaceBehavior { NONE, WITH_NON_AIR, ALL } ``` and: ```java public record PasteOptions( Location origin, boolean placeEntities, boolean placeTileEntities, boolean placePendingTicks, boolean deferredPhysics, boolean observersLast, int maxBlocksPerChunkTask, int yawRotation, ReplaceBehavior replaceBehavior, BiConsumer<Player, String> progress ) {} ``` Parse the standard protocol field: ```text ReplaceMode ``` for both: - inline `LitematicaPaste` - transmitted/sliced Direct Paste Then implement the three upstream semantics. ### `WITH_NON_AIR` Skip schematic air: ```java if (schematicAir) { continue; } ``` ### `ALL` Do **not** discard schematic air. Air should become a normal planned write so that existing destination blocks can be removed. ### `NONE` Before writing a schematic non-air block: ```java Block destination = world.getBlockAt(pw.x, pw.y, pw.z); if (!destination.getType().isAir()) { continue; } ``` Ideally the exact behavior should follow upstream Servux/Litematica's `ReplaceBehavior` implementation rather than introducing different semantics. --- # 2. Entity positions are offset incorrectly This is reproducible with entities such as minecarts. Blocks land at the correct placement coordinates, while entities may appear offset from the schematic. ## Suspected source-level cause In `PasteOperation.planRegion()` the entity coordinates are effectively handled as: ```java double lx = doubleOf(posList.get(0)) - region.originX; double ly = doubleOf(posList.get(1)) - region.originY; double lz = doubleOf(posList.get(2)) - region.originZ; ``` However, Litematica stores entity `Pos` values relative to the region origin when saving a schematic. The client-side save logic effectively does: ```java Vec3d relPos = new Vec3d( entityX - regionOriginX, entityY - regionOriginY, entityZ - regionOriginZ ); ``` Therefore the values in the entity `Pos` list are already region-local. LitematicaFolia later calculates the region world start using the region position again: ```java int[] rotRO = rotateXZ(region.originX, region.originZ, yaw); int startX = origin.getBlockX() + rotRO[0]; int startY = origin.getBlockY() + region.originY; int startZ = origin.getBlockZ() + rotRO[1]; ``` Subtracting `region.originX/Y/Z` from entity `Pos` a second time therefore applies the region offset incorrectly. For example: ```text region Position X = 100 entity local Pos X = 2.5 current: lx = 2.5 - 100 = -97.5 startX = pasteOriginX + 100 final worldX ~= pasteOriginX + 2.5 ``` Instead of: ```text worldX = pasteOriginX + 100 + 2.5 ``` --- ## Possible fix Do not subtract the region position again: ```java double lx = doubleOf(posList.get(0)); double ly = doubleOf(posList.get(1)); double lz = doubleOf(posList.get(2)); ``` Then transform the region-local entity position using the same effective placement transform as the region blocks. Once per-sub-region overrides are implemented, entity coordinates should use that same effective sub-region transform as well. --- # 3. Fractional entity coordinates are rotated incorrectly Current logic appears to rotate only the floored integer portion of the entity position and then re-add the original fractional components. Conceptually: ```java int[] rxz = rotateXZdouble(lx, lz, yaw); double wx = startX + rxz[0] + (lx - Math.floor(lx)); double wz = startZ + rxz[1] + (lz - Math.floor(lz)); ``` with logic equivalent to: ```java private static int[] rotateXZdouble(double x, double z, int yaw) { return rotateXZ( (int) Math.floor(x), (int) Math.floor(z), yaw ); } ``` This means the integer coordinate is rotated, but the fractional X/Z components remain associated with their original axes. Example: ```text (x, z) = (2.5, 3.25) correct 90-degree transform: (-3.25, 2.5) ``` The current integer-plus-original-fraction approach cannot produce the correct transformed coordinates. --- ## Possible fix Use a real double-coordinate transform: ```java private static double[] rotateXZDouble(double x, double z, int yaw) { return switch (yaw) { case 90 -> new double[] {-z, x}; case 180 -> new double[] {-x, -z}; case 270 -> new double[] { z, -x}; default -> new double[] { x, z}; }; } ``` Then: ```java double[] rotated = rotateXZDouble(lx, lz, yaw); double wx = startX + rotated[0]; double wy = startY + ly; double wz = startZ + rotated[1]; ``` The chunk key can still be calculated afterwards: ```java int cx = ((int) Math.floor(wx)) >> 4; int cz = ((int) Math.floor(wz)) >> 4; ``` --- # 4. Entity yaw/orientation is not transformed In the 26.2 NMS bridge, the entity is loaded from NBT and moved to the requested location while preserving its original yaw/pitch, conceptually: ```java entity.snapTo( loc.getX(), loc.getY(), loc.getZ(), entity.getYRot(), entity.getXRot() ); ``` If the whole schematic is rotated by 90/180/270 degrees, the blocks are rotated but the entity's own orientation is not adjusted. This may affect: - minecarts - armor stands - display entities - mobs - other directional entities --- ## Possible fix Apply the effective placement transform to entity orientation before spawning. For rotation-only behavior, conceptually: ```java float transformedYaw = normalizeYaw(entity.getYRot() + placementYaw); ``` then: ```java entity.snapTo( loc.getX(), loc.getY(), loc.getZ(), transformedYaw, entity.getXRot() ); ``` If global/sub-region mirroring is supported, entity orientation should use the same mirror + rotation transformation semantics as upstream Litematica/Servux instead of merely adding yaw. --- # 5. `SubRegions` placement overrides appear to be ignored This appears to be another important Servux compatibility issue. The Direct Paste handler already recognizes that `SubRegions` contains per-region placement overrides rather than block data. However, the effective sub-region placement does not appear to be reconstructed before `PasteOperation` runs. The implementation primarily loads the raw schematic data from: ```text Schematics ``` and starts the paste operation. This means a client placement that modifies an individual sub-region may still paste the original raw schematic-region layout. Potentially affected settings include: - sub-region enabled/disabled state - sub-region position - sub-region rotation - sub-region mirror - possibly sub-region entity settings --- ## Reproduction suggestion Use a schematic with two sub-regions. In the Litematica placement configuration: 1. disable one region, or 2. move one region, or 3. rotate one region Then Direct Paste the schematic. ### Expected The server-side paste should exactly match the placement visible on the client. ### Suspected current behavior The raw `.litematic` region arrangement may be pasted instead of the effective `SubRegions` placement state. --- ## Possible fix Instead of treating: ```text Schematics ``` as the complete placement definition, reconstruct an effective placement model from fields such as: ```text Origin Rotation Mirror SubRegions IgnoreEntities ``` The effective transformation should conceptually be: ```text world position = placement origin + global placement transform(sub-region position) + sub-region transform(local schematic coordinate) ``` Disabled sub-regions should not be planned at all. Upstream Servux implementations such as: ```text SchematicPlacement.createFromData(...) TaskPasteSchematicPerChunkDirect ``` may provide useful reference semantics. --- # 6. Other standard Servux paste settings are not applied Litematica sends standard placement state including at least: ```text ReplaceMode PasteLayerBehavior RenderLayerRange ``` and upstream Servux parses these fields. LitematicaFolia currently has no corresponding representation for them in `PasteOptions`. As a result, layer-limited pasting is also likely to differ from upstream Servux. For example, if the user selects a limited paste/render layer range, Direct Paste may still paste the entire schematic. --- ## Possible fix Add the relevant Servux placement settings to a protocol-level placement model and filter planned blocks before populating the per-chunk write queues. It may also be useful to audit the remaining upstream `LitematicaPaste` fields for parity, including where applicable: ```text ChangedBlocksOnly IgnoreBlocks Mirror ``` --- # 7. `maxBlocksPerChunkTask` appears to be unused The config currently describes: ```yaml paste: # Hard cap on blocks placed per chunk-task before yielding back to the # region scheduler. Prevents long ticks on huge schematics. maxBlocksPerChunkTask: 8192 ``` `PasteOptions` also exposes: ```java int maxBlocksPerChunkTask ``` but it appears to be advisory only. The block application path iterates the entire per-chunk write collection in a single scheduled operation rather than using the configured limit to yield. For a very large/dense chunk, this means all writes may still execute in one region scheduler task. --- ## Possible fix Split each per-chunk write list into batches of at most: ```java options.maxBlocksPerChunkTask() ``` and reschedule/yield the remaining work on the same owning region. Care should be taken to preserve paste phases/order, for example: ```text pass 1 normal blocks ↓ tile entities ↓ pass 2 / active blocks ↓ entities + pending ticks ↓ physics/update sweep ``` --- # Suggested architectural approach Instead of continuing to add individual special cases to `DirectPasteHandler`, it may be cleaner to introduce a protocol-level placement description that mirrors the semantic data already provided by Litematica/Servux. For example: ```java record ServuxPlacementOptions( Location origin, Rotation rotation, Mirror mirror, ReplaceBehavior replaceBehavior, PasteLayerBehavior pasteLayerBehavior, LayerRange layerRange, boolean ignoreEntities, Map<String, SubRegionOptions> subRegions ) {} ``` Then both Direct Paste transports: ```text inline LitematicaPaste Litematic-Transmit / sliced paste ``` could decode into the same effective placement model before invoking `PasteOperation`. `PasteOperation` would then be responsible only for applying an already-decoded placement. This would also reduce behavior differences between the inline and sliced upload paths. --- # Upstream references The expected protocol behavior can be compared against upstream Litematica and Servux. ## Litematica `SchematicPlacement` serializes placement state such as: ```text ReplaceMode PasteLayerBehavior RenderLayerRange SubRegions ``` The Direct Paste path subsequently transmits this placement metadata to the server. ## Servux 26.2 `fi.dy.masa.servux.dataproviders.LitematicsDataProvider` parses fields including: ```text ReplaceMode PasteLayerBehavior RenderLayerRange ChangedBlocksOnly IgnoreBlocks IgnoreEntities ``` and uses them when constructing the server-side paste task. Because these semantics already exist in the upstream Litematica <-> Servux protocol, matching them in the bridge seems preferable to introducing plugin-specific interpretations. --- # Expected result Servux Direct Paste through LitematicaFolia should reproduce the same effective placement that the Litematica client displays and that upstream Fabric + Servux would paste: - correct `NONE / WITH_NON_AIR / ALL` replace semantics - correct global placement rotation/mirror - correct sub-region enabled/position/rotation/mirror state - correct layer/range settings - correct entity world coordinates - correct fractional entity coordinate transforms - correct entity orientation - consistent behavior between inline and transmitted/sliced Direct Paste paths - actual yielding according to `maxBlocksPerChunkTask` Thanks for the work on bringing native `.litematic` / Servux support to Paper/Folia. The core Direct Paste transport itself is working well. These issues appear to be concentrated mainly in translating the full Litematica/Servux placement semantics into the current `PasteOptions` / `PasteOperation` pipeline.
stavapin stopped working 2026-08-27 12:15:54 +00:00
55 seconds
stavapin deleted spent time 2026-08-27 12:16:09 +00:00
- 55 seconds
Owner

Hi @stavapin, thank you for an exceptionally detailed and accurate report. Every point checked out against the code and upstream, and it is now fixed and released in 0.8.0+26.2:

  • ReplaceMode None / With-non-air / All are honored
  • entity positions are no longer double-offset, and fractional coordinates rotate correctly
  • entity orientation now rotates and mirrors with the placement
  • per-sub-region overrides (enabled / position / rotation / mirror) are applied
  • PasteLayerBehavior and RenderLayerRange are respected
  • maxBlocksPerChunkTask now actually paces large pastes instead of being advisory

One note on your suggested fix for the fractional-coordinate rotation: a plain negation is off by one block cell, so I used the upstream cell-preserving convention (1.0 - v) instead. The audit also turned up two related things now fixed: block states such as stairs rotate and mirror correctly, and structure_void is never pasted. A /litematica save edge case (an entity straddling a chunk border being captured twice) is fixed too.

Validated with 103 unit tests, a 56-check fidelity suite across rotation / mirror / sub-region / layer / entity scenarios, and a real Litematica client paste. The same fixes also ship in 0.9.0+1.21.11 for 1.21.11 servers. Thanks again for the report.

Hi @stavapin, thank you for an exceptionally detailed and accurate report. Every point checked out against the code and upstream, and it is now fixed and released in 0.8.0+26.2: - ReplaceMode None / With-non-air / All are honored - entity positions are no longer double-offset, and fractional coordinates rotate correctly - entity orientation now rotates and mirrors with the placement - per-sub-region overrides (enabled / position / rotation / mirror) are applied - PasteLayerBehavior and RenderLayerRange are respected - maxBlocksPerChunkTask now actually paces large pastes instead of being advisory One note on your suggested fix for the fractional-coordinate rotation: a plain negation is off by one block cell, so I used the upstream cell-preserving convention (1.0 - v) instead. The audit also turned up two related things now fixed: block states such as stairs rotate and mirror correctly, and structure_void is never pasted. A /litematica save edge case (an entity straddling a chunk border being captured twice) is fixed too. Validated with 103 unit tests, a 56-check fidelity suite across rotation / mirror / sub-region / layer / entity scenarios, and a real Litematica client paste. The same fixes also ship in 0.9.0+1.21.11 for 1.21.11 servers. Thanks again for the report.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
admin_ekaii/litematica-folia-ekaii#4
No description provided.