Add configs, store blocks after being burned for queued restore later

This commit is contained in:
zontreck 2024-01-21 22:06:29 -07:00
parent 0ab76914c4
commit 80e068c198
8 changed files with 403 additions and 68 deletions

View file

@ -3,7 +3,7 @@
org.gradle.jvmargs=-Xmx3G
org.gradle.daemon=false
libzontreck=1.10.011624.1712
libzontreck=1.10.012124.1709
## Environment Properties
@ -49,7 +49,7 @@ mod_name=Fire! Fire!
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=GPLv3
# The mod version. See https://semver.org/
mod_version=1.0.012124.0305
mod_version=1.0.012124.2105
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html

View file

@ -1,11 +1,14 @@
package dev.zontreck.fire;
import com.mojang.logging.LogUtils;
import dev.zontreck.fire.config.server.FireServerConfig;
import dev.zontreck.fire.data.BlockRestoreData;
import dev.zontreck.fire.events.EventHandler;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.RegistryEvent;
import net.minecraftforge.event.server.ServerStartedEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.InterModComms;
import net.minecraftforge.fml.common.Mod;
@ -26,41 +29,24 @@ public class FireMod
// Directly reference a slf4j logger
public static final Logger LOGGER = LogUtils.getLogger();
public static boolean ENABLED = false; // Turned to false when shutting down the server
public static BlockRestoreData blockRestoreData;
public FireMod()
{
// Register the setup method for modloading
FMLJavaModLoadingContext.get().getModEventBus().addListener(this::setup);
// Register the enqueueIMC method for modloading
FMLJavaModLoadingContext.get().getModEventBus().addListener(this::enqueueIMC);
// Register the processIMC method for modloading
FMLJavaModLoadingContext.get().getModEventBus().addListener(this::processIMC);
FireServerConfig.load();
// Register ourselves for server and other game events we are interested in
MinecraftForge.EVENT_BUS.register(this);
MinecraftForge.EVENT_BUS.register(new EventHandler());
}
private void setup(final FMLCommonSetupEvent event)
{
// some preinit code
LOGGER.info("HELLO FROM PREINIT");
LOGGER.info("DIRT BLOCK >> {}", Blocks.DIRT.getRegistryName());
}
private void enqueueIMC(final InterModEnqueueEvent event)
{
// Some example code to dispatch IMC to another mod
//InterModComms.sendTo("fire", "helloworld", () -> { LOGGER.info("Hello world from the MDK"); return "Hello world";});
}
private void processIMC(final InterModProcessEvent event)
{
// Some example code to receive and process InterModComms from other mods
//LOGGER.info("Got IMC {}", event.getIMCStream().
// map(m->m.messageSupplier().get()).
// collect(Collectors.toList()));
}
// You can use SubscribeEvent and let the Event Bus discover methods to call
@ -71,6 +57,15 @@ public class FireMod
LOGGER.info("HELLO from server starting");
}
@SubscribeEvent
public void onServerReady(ServerStartedEvent event)
{
ENABLED=true;
blockRestoreData = BlockRestoreData.load(event.getServer());
MinecraftForge.EVENT_BUS.register(new EventHandler());
}
// You can use EventBusSubscriber to automatically subscribe events on the contained class (this is subscribing to the MOD
// Event bus for receiving Registry Events)
@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)

View file

@ -0,0 +1,43 @@
package dev.zontreck.fire.config.server;
import dev.zontreck.fire.config.server.sections.RestoreSection;
import dev.zontreck.fire.data.FireDatastore;
import dev.zontreck.libzontreck.util.SNbtIo;
import net.minecraft.nbt.CompoundTag;
import java.nio.file.Path;
public class FireServerConfig
{
public static final Path FILE = FireDatastore.of("config.snbt", false);
public static RestoreSection restore;
public static void load()
{
if(!FILE.toFile().exists())
{
initialize();
}
CompoundTag tag = SNbtIo.loadSnbt(FILE);
if(tag.contains(RestoreSection.TAG_NAME))
{
restore = RestoreSection.deserialize(tag.getCompound(RestoreSection.TAG_NAME));
} else restore = new RestoreSection();
}
private static void initialize()
{
restore = new RestoreSection();
save();
}
public static void save()
{
CompoundTag tag = new CompoundTag();
tag.put(RestoreSection.TAG_NAME, restore.serialize());
SNbtIo.writeSnbt(FILE, tag);
}
}

View file

@ -0,0 +1,31 @@
package dev.zontreck.fire.config.server.sections;
import net.minecraft.nbt.CompoundTag;
public class RestoreSection
{
public static final String TAG_NAME = "restore";
public static final String TAG_CONTAINERS = "containers";
public static final String TAG_DELAY = "delay";
public boolean restoreContainers = false;
public int delayForRestore = 120;
public static RestoreSection deserialize(CompoundTag tag)
{
RestoreSection sect = new RestoreSection();
sect.restoreContainers = tag.getBoolean(TAG_CONTAINERS);
sect.delayForRestore = tag.getInt(TAG_DELAY);
return sect;
}
public CompoundTag serialize()
{
CompoundTag tag = new CompoundTag();
tag.putBoolean(TAG_CONTAINERS, restoreContainers);
tag.putInt(TAG_DELAY, delayForRestore);
return tag;
}
}

View file

@ -0,0 +1,120 @@
package dev.zontreck.fire.data;
import dev.zontreck.libzontreck.LibZontreck;
import dev.zontreck.libzontreck.util.SNbtIo;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.server.MinecraftServer;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class BlockRestoreData
{
public static Path BASE;
public Map<BlockPos, BlockSnapshot> snapshots = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public static BlockRestoreData load(MinecraftServer server)
{
BASE = FireDatastore.of("restore_" + server.getWorldData().getLevelName() + ".snbt", false);
if(!BASE.toFile().exists())
{
BlockRestoreData brd = new BlockRestoreData();
brd.init();
return brd;
}else {
BlockRestoreData brd = new BlockRestoreData();
brd.read();
return brd;
}
}
private void init()
{
snapshots = new HashMap<>();
}
private void read()
{
CompoundTag tag = new CompoundTag();
CompoundTag blocks = tag.getCompound("queue");
int count = blocks.size();
for(int i=0;i<count;i++)
{
CompoundTag cTag = blocks.getCompound(String.valueOf(i));
BlockSnapshot snap = BlockSnapshot.deserialize(cTag);
add(snap);
}
}
public void commit()
{
CompoundTag tag = new CompoundTag();
CompoundTag lst = new CompoundTag();
Lock lck = lock.readLock();
lck.lock();
try {
Iterator<Map.Entry<BlockPos, BlockSnapshot>> it = snapshots.entrySet().iterator();
int num=0;
while(it.hasNext())
{
var entry = it.next();
lst.put(String.valueOf(num), entry.getValue().serialize());
num++;
}
} finally {
lck.unlock();
}
tag.putInt("version", 1);
tag.put("queue", lst);
SNbtIo.writeSnbt(BASE, tag);
}
public void add(BlockSnapshot blockSnapshot) {
if(snapshots.containsKey(blockSnapshot.position.Position.asBlockPos()))
{
return;
}
Lock lck = lock.writeLock();
lck.lock();
try {
snapshots.put(blockSnapshot.position.Position.asBlockPos(), blockSnapshot);
}
finally {
lck.unlock();
}
}
public Lock acquireWriteLock()
{
return lock.writeLock();
}
public Lock acquireReadLock()
{
return lock.readLock();
}
}

View file

@ -0,0 +1,90 @@
package dev.zontreck.fire.data;
import dev.zontreck.libzontreck.exceptions.InvalidDeserialization;
import dev.zontreck.libzontreck.vectors.Vector3;
import dev.zontreck.libzontreck.vectors.WorldPosition;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.LongTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import java.time.Instant;
public class BlockSnapshot {
private final BlockState state;
private CompoundTag blockEntity;
protected final WorldPosition position;
public final long burnTime;
public BlockSnapshot(Level world, BlockPos pos) {
this.position = new WorldPosition(new Vector3(pos), (ServerLevel) world);
this.state = world.getBlockState(pos);
BlockEntity entity = world.getBlockEntity(pos);
if(entity != null)
this.blockEntity = world.getBlockEntity(pos).serializeNBT();
this.burnTime = Instant.now().getEpochSecond();
}
private BlockSnapshot(WorldPosition position, BlockState state, CompoundTag entity, long burn) {
this.position = position;
this.state = state;
this.blockEntity = entity;
this.burnTime = burn;
}
public void restore() {
Level world = position.getActualDimension();
world.setBlockAndUpdate(position.Position.asBlockPos(), state);
if (blockEntity != null) {
BlockEntity entity = world.getBlockEntity(position.Position.asBlockPos());
if(entity != null)
{
entity.load(blockEntity);
}
}
world.playSound(null, position.Position.asBlockPos(), SoundEvents.ITEM_PICKUP, SoundSource.NEUTRAL, world.random.nextFloat(0,1), world.random.nextFloat(0,1));
}
public CompoundTag serialize()
{
CompoundTag tag = new CompoundTag();
tag.put("state", NbtUtils.writeBlockState(state));
if(blockEntity!=null)
tag.put("entity", blockEntity);
tag.put("pos", position.serializePretty());
tag.put("burnedAt", LongTag.valueOf(burnTime));
return tag;
}
public static BlockSnapshot deserialize(CompoundTag tag)
{
try {
WorldPosition position = new WorldPosition(tag.getCompound("pos"), true);
BlockState state = NbtUtils.readBlockState(tag.getCompound("state"));
CompoundTag entity = null;
if(tag.contains("entity"))
entity = tag.getCompound("entity");
long burn = tag.getLong("burnedAt");
return new BlockSnapshot(position, state, entity, burn);
} catch (InvalidDeserialization e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,30 @@
package dev.zontreck.fire.data;
import dev.zontreck.libzontreck.util.FileTreeDatastore;
import java.nio.file.Path;
public class FireDatastore extends FileTreeDatastore
{
public static final Path FBASE;
static {
FBASE = of("fire");
if(!FBASE.toFile().exists())
{
FBASE.toFile().mkdirs();
}
}
public static Path of(String nick, boolean directory)
{
Path nPath = FBASE.resolve(nick);
if(directory)
{
if(!nPath.toFile().exists())
{
nPath.toFile().mkdir();
}
}
return nPath;
}
}

View file

@ -1,20 +1,21 @@
package dev.zontreck.fire.events;
import dev.zontreck.fire.FireMod;
import dev.zontreck.fire.config.server.FireServerConfig;
import dev.zontreck.fire.data.BlockSnapshot;
import dev.zontreck.libzontreck.util.ServerUtilities;
import dev.zontreck.libzontreck.vectors.Vector3;
import dev.zontreck.libzontreck.vectors.WorldPosition;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.level.Level;
import net.minecraft.world.Container;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.ChestBlock;
import net.minecraft.world.level.block.entity.BaseContainerBlockEntity;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.server.ServerStoppingEvent;
import net.minecraftforge.event.world.BlockEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
@ -22,17 +23,23 @@ import java.time.Instant;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
public class EventHandler {
private static final Map<BlockPos, BlockSnapshot> burnedBlocks = new HashMap<>();
static {
MinecraftForge.EVENT_BUS.register(EventHandler.class);
}
private static final AtomicLong ALIVE_TICKS = new AtomicLong(0);
@SubscribeEvent
public void onBlockBreak(BlockEvent.NeighborNotifyEvent event) {
if (ServerUtilities.isClient()) return;
if(!FireMod.ENABLED)
{
return;
}
ServerLevel world = (ServerLevel) event.getWorld();
BlockPos pos = event.getPos();
@ -77,69 +84,88 @@ public class EventHandler {
blockState = world.getBlockState(pos);
if (blockState.is(Blocks.FIRE) || blockState.isAir()) {
continue; // DO NOT CACHE FIRE OR AIR BLOCKS
if (blockState.is(Blocks.FIRE) || blockState.isAir() || !blockState.isFlammable(world, pos, dir)) {
continue; // DO NOT CACHE FIRE OR AIR BLOCKS, OR NON-FLAMMABLE BLOCKS
}
// Do not cache containers and their inventories. This could be used as a duplication exploit in that case
BlockEntity entity = world.getBlockEntity(pos);
if(entity instanceof BaseContainerBlockEntity && FireServerConfig.restore.restoreContainers)
{
if(blockState.isFlammable(world, pos, dir))
{
// We're caching it, remove it from the world immediately to prevent a dupe. It will be restored afterwards
world.setBlock(pos, Blocks.AIR.defaultBlockState(), ChestBlock.UPDATE_ALL);
}
}
//FireMod.LOGGER.info("Fire detected");
burnedBlocks.put(pos, new BlockSnapshot(world, pos));
FireMod.blockRestoreData.add(new BlockSnapshot(world, pos));
}
FireMod.blockRestoreData.commit();
}
@SubscribeEvent
public void onServerTick(TickEvent.WorldTickEvent event) {
if(ServerUtilities.isClient()) return;
if(!FireMod.ENABLED)
{
return;
}
if (event.phase == TickEvent.Phase.END) {
restoreBurnedBlocks();
if(ALIVE_TICKS.getAndIncrement() % 10 == 0)
{
restoreBurnedBlocks();
FireMod.blockRestoreData.commit();
}
}
}
@SubscribeEvent
public void onServerStopping(ServerStoppingEvent event)
{
if(ServerUtilities.isClient()) return;
FireMod.ENABLED=false;
FireMod.blockRestoreData.commit();
}
public void restoreBurnedBlocks() {
long currentTime = Instant.now().getEpochSecond();
// Restore one block per tick, after it hasnt burned for long enough
Iterator<Map.Entry<BlockPos, BlockSnapshot>> it = burnedBlocks.entrySet().iterator();
while(it.hasNext())
{
Lock lock = FireMod.blockRestoreData.acquireWriteLock();
lock.lock();
var entry = it.next();
if((currentTime - entry.getValue().burnTime) >= 60 + (burnedBlocks.size()*2))
try {
// Restore one block per tick, after it hasnt burned for long enough
Iterator<Map.Entry<BlockPos, BlockSnapshot>> it = FireMod.blockRestoreData.snapshots.entrySet().iterator();
while(it.hasNext())
{
it.remove();
entry.getValue().restore();
var entry = it.next();
//FireMod.LOGGER.info("Restoring burned block");
if((currentTime - entry.getValue().burnTime) >= FireServerConfig.restore.delayForRestore + (FireMod.blockRestoreData.snapshots.size()))
{
it.remove();
entry.getValue().restore();
return;
//FireMod.LOGGER.info("Restoring burned block");
return;
}
}
}finally {
lock.unlock();
}
}
private static class BlockSnapshot {
private final BlockState state;
private final BlockEntity blockEntity;
private final WorldPosition position;
public final long burnTime;
public BlockSnapshot(Level world, BlockPos pos) {
this.position = new WorldPosition(new Vector3(pos), (ServerLevel) world);
this.state = world.getBlockState(pos);
this.blockEntity = world.getBlockEntity(pos);
this.burnTime = Instant.now().getEpochSecond();
}
public void restore() {
Level world = position.getActualDimension();
world.setBlockAndUpdate(position.Position.asBlockPos(), state);
if (blockEntity != null) {
world.setBlockEntity(blockEntity);
}
world.playLocalSound(position.Position.x, position.Position.y, position.Position.z, SoundEvents.ITEM_PICKUP, SoundSource.NEUTRAL, world.random.nextFloat(0,1), world.random.nextFloat(0,1), true);
}
}
}