Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
97 changes: 93 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# TileEntityReplacementManager API
# Postea

This library provides a suite of tools for transforming any existing tile entities, blocks and items the world at
runtime. Allowing developers to replace, migrate or modify game elements based on specific conditions.
Expand All @@ -11,6 +11,9 @@ runtime. Allowing developers to replace, migrate or modify game elements based o
4. **Simple Replacement API**: Efficiently replace or remap large amounts of blocks and items at scale without incurring any noticeable performance impact.
5. **Missing Mapping Replacement API**: A set of API endpoints to register actions to be taken when FML detects a missing mapping for a given ID.
6. **Numeric ID Identification for Removed Content**: A way to identify the ID of any content that has ceased to exist.
7. **Versioned Chunk and Player Transformers**: transform a chunk or a player's data exactly once per version of your content, however many sessions it was saved across.
8. **Retired Id Table**: names of removed content keep resolving in every later session.
9. **Custom World Data Transformers**: versioned transformation of a mod's own world storage, offered by the mod that owns it.

## Examples

Expand All @@ -30,9 +33,9 @@ transformed.

> [!TIP]
>
> Tile entity and block transformers are only ran once per chunk until a player's mod list changes. This behaviour is
> disabled if you are in a dev environment; Allowing you to test your transformers simply by leaving and rejoining your
> test world.
> Tile entity, block, and item transformers run once per chunk until the mod list changes (disabled in a dev
> environment). For transformations that must run exactly once per version of your own content, use a versioned
> transformer (§9).

> [!CAUTION]
>
Expand Down Expand Up @@ -514,3 +517,89 @@ public abstract class FMLIgnoreMissingMappingExample {
}
}
```

### 9. Versioned Transformers

Postea provides some API endpoints to allow mods to have versioned changes. The mod declares that it
expects some version, and if Postea detects that a chunk or player was saved on a previous version, runs a transformer
to bring them up-to-date. Register an `IVersionedTransformer` through `VersionedReplacementManager.register`.

Some mods keep item stacks stored outside chunks or player data. Postea provides an API, `CustomDataReplacementManager`,
for those mods to allow that data to be transformed by an `IVersionedTransformer`.

> [!IMPORTANT]
>
> Versioned transformers run before every other Postea pass, in registration order, and are never disabled in a dev
> environment. Data saved before a transformer existed reads as `UNSTAMPED`; the transformer decides what that means.

> [!CAUTION]
>
> Chunk transformers may run on a chunk I/O thread, several concurrently for different chunks. They must not touch
> world state beyond the context, and their own state must be safe to read concurrently. As with the other chunk-read
> passes, `ChunkTransformContext.world()` cannot be used for block access.
>
> When a transformer changes a block id, Postea recomputes the chunk's height map and flags it for a relight once
> every transformer has run; nothing is recomputed for metadata-only changes.
>
> An exception thrown by a transformer crashes the game, with the key, both versions, and the chunk or player named in
> the crash report. This is deliberate: a half-transformed chunk stamped at the current version could never be
> recovered.

Code example:
```java
public final class VersionedExample implements IVersionedTransformer {

public static void postLoad() {
VersionedReplacementManager.register(new VersionedExample());
}

@Override
public String key() {
return "examplemod:gemIndex";
}

// The version the running mod writes gems under; bump it whenever the index assignment changes.
@Override
public int currentVersion() {
return 2;
}

@Override
public void transformChunk(ChunkTransformContext ctx) {
// Data saved before this transformer existed carries no stamp.
if (ctx.storedVersion() == ChunkTransformContext.UNSTAMPED || ctx.storedVersion() == 1) {
int gemBlockId = Block.getIdFromBlock(ExampleMod.gemBlock);
ctx.forEachBlock((x, y, z, id, meta) -> {
if (id == gemBlockId) ctx.setBlock(x, y, z, id, meta + 1);
});
ctx.forEachItemStackTag(stack -> {
if (IDExtenderCompat.getItemStackID(stack) == Item.getIdFromItem(ExampleMod.gemItem)) {
stack.setShort("Damage", (short) (stack.getShort("Damage") + 1));
}
});
}
}

@Override
public void transformPlayer(PlayerDataTransformContext ctx) {
if (ctx.storedVersion() == PlayerDataTransformContext.UNSTAMPED || ctx.storedVersion() == 1) {
ctx.forEachItemStackTag(stack -> {
if (IDExtenderCompat.getItemStackID(stack) == Item.getIdFromItem(ExampleMod.gemItem)) {
stack.setShort("Damage", (short) (stack.getShort("Damage") + 1));
}
});
}
}
}
```

### 10. Retired Ids

Whenever a world loads, every block or item name in its saved id map that no longer exists is recorded, with the id it
held, in `<world>/postea/known-ids.json`. This way, retired objects can still be referenced by transformers.

When a name resolves only through retired ids, Postea logs it once per world load:

```
Block ExtraUtilities:cobblestone_compressed is no longer registered; its transformers target retired id(s) [3402]
```
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
plugins {
id 'com.gtnewhorizons.gtnhconvention'
}

test {
useJUnitPlatform()
}
7 changes: 6 additions & 1 deletion dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
dependencies {
api("com.github.GTNewHorizons:GTNHLib:0.9.39:dev")
compileOnly('com.github.GTNewHorizons:NotEnoughIds:2.1.10:dev')
compileOnly('com.falsepattern:chunkapi-mc1.7.10:0.8.2:dev')
compileOnly('com.falsepattern:endlessids-mc1.7.10:1.7.1:dev')
compileOnly("com.github.GTNewHorizons:NotEnoughItems:2.8.75-GTNH:dev") { transitive = false }

testImplementation(platform('org.junit:junit-bom:5.11.4'))
testImplementation('org.junit.jupiter:junit-jupiter')
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
// fastutil is only on the compile/runtime classpath via the patched Minecraft jar, not the plain test JVM.
testRuntimeOnly('it.unimi.dsi:fastutil:8.5.18')
}
48 changes: 48 additions & 0 deletions src/main/java/com/gtnewhorizons/postea/ExampleMigrators.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@

import com.gtnewhorizons.postea.api.BlockAccessCompat;
import com.gtnewhorizons.postea.api.BlockReplacementManager;
import com.gtnewhorizons.postea.api.ChunkTransformContext;
import com.gtnewhorizons.postea.api.IDExtenderCompat;
import com.gtnewhorizons.postea.api.IVersionedTransformer;
import com.gtnewhorizons.postea.api.ItemStackReplacementManager;
import com.gtnewhorizons.postea.api.PlayerDataTransformContext;
import com.gtnewhorizons.postea.api.TileEntityReplacementManager;
import com.gtnewhorizons.postea.api.VersionedReplacementManager;
import com.gtnewhorizons.postea.utility.BlockConversionInfo;
import com.gtnewhorizons.postea.utility.BlockInfo;

Expand All @@ -32,6 +36,7 @@ static void postLoad() {
ComplexBlockTransformer.postLoad();
ComplexItemTransformer.postLoad();
FMLReMappingExample.postLoad();
VersionedExample.postLoad();
}

public static abstract class TEToBlockExample {
Expand Down Expand Up @@ -347,4 +352,47 @@ public static void postLoad() {
BlockReplacementManager.ignoreMissingMapping("IC2:blockCrop");
}
}

public static final class VersionedExample implements IVersionedTransformer {

public static void postLoad() {
VersionedReplacementManager.register(new VersionedExample());
}

@Override
public String key() {
return "examplemod:gemIndex";
}

// The version the running mod writes gems under; bump it whenever the index assignment changes.
@Override
public int currentVersion() {
return 2;
}

@Override
public void transformChunk(ChunkTransformContext ctx) {
// Data saved before this transformer existed carries no stamp.
if (ctx.storedVersion() == ChunkTransformContext.UNSTAMPED || ctx.storedVersion() == 1) {
int gemBlockId = Block.getIdFromBlock(Blocks.wool);
ctx.forEachBlock((x, y, z, id, meta) -> { if (id == gemBlockId) ctx.setBlock(x, y, z, id, meta + 1); });
ctx.forEachItemStackTag(stack -> {
if (IDExtenderCompat.getItemStackID(stack) == Item.getIdFromItem(Items.dye)) {
stack.setShort("Damage", (short) (stack.getShort("Damage") + 1));
}
});
}
}

@Override
public void transformPlayer(PlayerDataTransformContext ctx) {
if (ctx.storedVersion() == PlayerDataTransformContext.UNSTAMPED || ctx.storedVersion() == 1) {
ctx.forEachItemStackTag(stack -> {
if (IDExtenderCompat.getItemStackID(stack) == Item.getIdFromItem(Items.dye)) {
stack.setShort("Damage", (short) (stack.getShort("Damage") + 1));
}
});
}
}
}
}
27 changes: 27 additions & 0 deletions src/main/java/com/gtnewhorizons/postea/Postea.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
package com.gtnewhorizons.postea;

import java.io.File;

import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.world.ChunkEvent;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import com.gtnewhorizons.postea.utility.ChunkFixerUtility;
import com.gtnewhorizons.postea.utility.IDRegistry;
import com.gtnewhorizons.postea.utility.MissingMappingHandler;
import com.gtnewhorizons.postea.utility.SimpleTransformationRegistry;
import com.gtnewhorizons.postea.utility.TransformerRegistry;
import com.gtnewhorizons.postea.utility.VersionedTransformerLog;

import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.event.FMLLoadCompleteEvent;
import cpw.mods.fml.common.event.FMLMissingMappingsEvent;
import cpw.mods.fml.common.event.FMLModIdMappingEvent;
import cpw.mods.fml.common.event.FMLPostInitializationEvent;
import cpw.mods.fml.common.event.FMLPreInitializationEvent;
import cpw.mods.fml.common.event.FMLServerAboutToStartEvent;
import cpw.mods.fml.common.event.FMLServerStoppedEvent;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;

@Mod(
Expand All @@ -29,6 +38,8 @@ public class Postea {
public static final String MODID = "postea";
public static final String MODNAME = "Postea";

public static final Logger LOG = LogManager.getLogger(MODNAME);

@Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) {
MinecraftForge.EVENT_BUS.register(this);
Expand All @@ -44,6 +55,22 @@ public void chunkLoaded(ChunkEvent.Load event) {
ChunkFixerUtility.onChunkLoaded(event.getChunk());
}

@Mod.EventHandler
public void serverAboutToStart(FMLServerAboutToStartEvent event) {
IDRegistry.beginWorld(
new File(
FMLCommonHandler.instance()
.getSavesDirectory(),
event.getServer()
.getFolderName()));
}

@Mod.EventHandler
public void serverStopped(FMLServerStoppedEvent event) {
IDRegistry.endWorld();
VersionedTransformerLog.clear();
}

@Mod.EventHandler
public void onIdMappingsChanged(FMLModIdMappingEvent event) {
SimpleTransformationRegistry.onIdMappingsChanged();
Expand Down
Loading