/*
* Copyright © 2016 cpw
* This file is part of Simpleretrogen.
*
* Simpleretrogen 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.
*
* Simpleretrogen 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 Simpleretrogen. If not, see <http://www.gnu.org/licenses/>.
*/
package cpw.mods.retro;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import net.minecraft.command.CommandBase;
import net.minecraft.command.CommandException;
import net.minecraft.command.ICommandSender;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.text.TextComponentString;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.world.World;
import net.minecraft.world.WorldServer;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.IChunkGenerator;
import net.minecraft.world.chunk.IChunkProvider;
import net.minecraft.world.gen.ChunkProviderServer;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.config.Configuration;
import net.minecraftforge.common.config.Property;
import net.minecraftforge.event.world.ChunkDataEvent;
import net.minecraftforge.fml.common.FMLLog;
import net.minecraftforge.fml.common.IWorldGenerator;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.Mod.EventHandler;
import net.minecraftforge.fml.common.ObfuscationReflectionHelper;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.event.FMLServerAboutToStartEvent;
import net.minecraftforge.fml.common.event.FMLServerStartingEvent;
import net.minecraftforge.fml.common.event.FMLServerStoppedEvent;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent;
import net.minecraftforge.fml.common.registry.GameRegistry;
import org.apache.logging.log4j.Level;
import javax.annotation.Nonnull;
import javax.annotation.ParametersAreNonnullByDefault;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Semaphore;
@Mod(modid="simpleretrogen", name="Simple Retrogen", acceptableRemoteVersions="*", acceptedMinecraftVersions = "[1.9,1.11)")
@ParametersAreNonnullByDefault
public class WorldRetrogen {
private List<Marker> markers = Lists.newArrayList();
private Map<String,TargetWorldWrapper> delegates;
private Map<World,ListMultimap<ChunkPos,String>> pendingWork;
private Map<World,ListMultimap<ChunkPos,String>> completedWork;
private ConcurrentMap<World,Semaphore> completedWorkLocks;
private int maxPerTick;
private Map<String,String> retros = Maps.newHashMap();
private static class Marker {
private final String marker;
private final Set<String> classes;
Marker(String marker, Set<String> classes) {
this.marker = marker;
this.classes = classes;
}
}
@EventHandler
public void preInit(FMLPreInitializationEvent evt)
{
Configuration cfg = new Configuration(evt.getSuggestedConfigurationFile(), null, true);
cfg.load();
Property property = cfg.get(Configuration.CATEGORY_GENERAL, "maxPerTick", 100);
property.setComment("Maximum number of retrogens to run in a single tick");
this.maxPerTick = property.getInt(100);
Property amProperty = cfg.get(Configuration.CATEGORY_GENERAL, "markerList", new String[0]);
amProperty.setComment("Active markers");
final List<String> activeMarkerList = Lists.newArrayList(amProperty.getStringList());
Set<String> categories = cfg.getCategoryNames();
if (categories.size() == 1) // only the general category - version 1 of config file
{
property = cfg.get(Configuration.CATEGORY_GENERAL, "worldGens", new String[0]);
String[] retros = property.getStringList();
property = cfg.get(Configuration.CATEGORY_GENERAL, "marker", "CPWRGMARK");
Marker m = new Marker(property.getString(), Sets.newHashSet(retros));
this.markers.add(m);
}
else
{
for (String marker : activeMarkerList) {
if (categories.contains(marker)) {
final Property property1 = cfg.get(marker, "worldGens", new String[0]);
property1.setComment("World Generator classes for marker");
this.markers.add(new Marker(marker, Sets.newHashSet(property1.getStringList())));
cfg.getCategory(marker).setComment("Marker definition\nYou can create as many of these as you wish\nActivate by adding to active list");
} else {
evt.getModLog().log(Level.INFO, "Ignoring missing marker definition for active marker %s", marker);
}
}
}
// clean up leftovers
cfg.getCategory(Configuration.CATEGORY_GENERAL).remove("worldGens");
cfg.getCategory(Configuration.CATEGORY_GENERAL).remove("marker");
for (Marker m : markers) {
for (String clz : m.classes) {
if (retros.put(clz, m.marker) != null) {
evt.getModLog().log(Level.ERROR, "Configuration error, duplicate class for multiple markers found : %s", clz);
}
}
if (!categories.contains(m.marker)) {
Property p = cfg.get(m.marker, "worldGens",new String[0]);
p.setComment("World Generator classes for marker");
p.set(m.classes.toArray(new String[0]));
cfg.getCategory(m.marker).setComment("Marker definition\nYou can create as many of these as you wish\nActivate by adding to active list");
if (!activeMarkerList.contains(m.marker)) {
activeMarkerList.add(m.marker);
amProperty.set(activeMarkerList.toArray(new String[0]));
}
}
}
if (cfg.hasChanged())
{
cfg.save();
}
MinecraftForge.EVENT_BUS.register(this);
MinecraftForge.EVENT_BUS.register(new LastTick());
this.delegates = Maps.newHashMap();
}
@EventHandler
public void serverStarting(FMLServerStartingEvent evt) {
evt.registerServerCommand(new CommandBase()
{
@Override
@Nonnull
public String getCommandName()
{
return "listretrogenclasstargets";
}
@Override
@Nonnull
public String getCommandUsage(ICommandSender sender)
{
return "List retrogens";
}
@Override
public int getRequiredPermissionLevel()
{
return 0;
}
@Override
public void execute(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException
{
Set<IWorldGenerator> worldGens = ObfuscationReflectionHelper.getPrivateValue(GameRegistry.class, null, "worldGenerators");
List<String> targets = Lists.newArrayList();
for (IWorldGenerator worldGen : worldGens) {
if (!(worldGen instanceof TargetWorldWrapper))
{
targets.add(worldGen.getClass().getName());
}
}
if (targets.isEmpty()) {
sender.addChatMessage(new TextComponentString("There are no retrogen target classes"));
} else {
sender.addChatMessage(new TextComponentString(CommandBase.joinNiceStringFromCollection(targets)));
}
}
});
}
@EventHandler
public void serverAboutToStart(FMLServerAboutToStartEvent evt)
{
this.pendingWork = new MapMaker().weakKeys().makeMap();
this.completedWork = new MapMaker().weakKeys().makeMap();
this.completedWorkLocks = new MapMaker().weakKeys().makeMap();
Set<IWorldGenerator> worldGens = ObfuscationReflectionHelper.getPrivateValue(GameRegistry.class, null, "worldGenerators");
Map<IWorldGenerator,Integer> worldGenIdx = ObfuscationReflectionHelper.getPrivateValue(GameRegistry.class, null, "worldGeneratorIndex");
for (String retro : ImmutableSet.copyOf(retros.keySet()))
{
if (!delegates.containsKey(retro))
{
FMLLog.info("Substituting worldgenerator %s with delegate", retro);
for (Iterator<IWorldGenerator> iterator = worldGens.iterator(); iterator.hasNext();)
{
IWorldGenerator wg = iterator.next();
if (wg.getClass().getName().equals(retro))
{
iterator.remove();
TargetWorldWrapper tww = new TargetWorldWrapper();
tww.delegate = wg;
tww.tag = retro;
worldGens.add(tww);
Integer idx = worldGenIdx.remove(wg);
worldGenIdx.put(tww, idx);
FMLLog.info("Successfully substituted %s with delegate", retro);
delegates.put(retro, tww);
break;
}
}
if (!delegates.containsKey(retro))
{
FMLLog.warning("WorldRetrogen was not able to locate world generator class %s, it will be skipped, found %s", retro, worldGens);
retros.remove(retro);
}
}
}
}
@EventHandler
public void serverStopped(FMLServerStoppedEvent evt)
{
Set<IWorldGenerator> worldGens = ObfuscationReflectionHelper.getPrivateValue(GameRegistry.class, null, "worldGenerators");
Map<IWorldGenerator,Integer> worldGenIdx = ObfuscationReflectionHelper.getPrivateValue(GameRegistry.class, null, "worldGeneratorIndex");
for (TargetWorldWrapper tww : delegates.values())
{
worldGens.remove(tww);
Integer idx = worldGenIdx.remove(tww);
worldGens.add(tww.delegate);
worldGenIdx.put(tww.delegate,idx);
}
delegates.clear();
}
private Semaphore getSemaphoreFor(World w)
{
completedWorkLocks.putIfAbsent(w, new Semaphore(1));
return completedWorkLocks.get(w);
}
private class LastTick {
private int counter = 0;
@SubscribeEvent
public void tickStart(TickEvent.WorldTickEvent tick)
{
World w = tick.world;
if (!(w instanceof WorldServer))
{
return;
}
if (tick.phase == TickEvent.Phase.START)
{
counter = 0;
getSemaphoreFor(w);
}
else
{
ListMultimap<ChunkPos, String> pending = pendingWork.get(w);
if (pending == null)
{
return;
}
ImmutableList<Entry<ChunkPos, String>> forProcessing = ImmutableList.copyOf(Iterables.limit(pending.entries(), maxPerTick + 1));
for (Entry<ChunkPos, String> entry : forProcessing)
{
if (counter++ > maxPerTick)
{
FMLLog.fine("Completed %d retrogens this tick. There are %d left for world %s", counter, pending.size(), w.getWorldInfo().getWorldName());
return;
}
runRetrogen((WorldServer)w, entry.getKey(), entry.getValue());
}
}
}
}
private class TargetWorldWrapper implements IWorldGenerator {
private IWorldGenerator delegate;
private String tag;
@Override
public void generate(Random random, int chunkX, int chunkZ, World world, IChunkGenerator chunkGenerator, IChunkProvider chunkProvider)
{
FMLLog.fine("Passing generation for %s through to underlying generator", tag);
delegate.generate(random, chunkX, chunkZ, world, chunkGenerator, chunkProvider);
ChunkPos chunkCoordIntPair = new ChunkPos(chunkX, chunkZ);
completeRetrogen(chunkCoordIntPair, world, tag);
}
}
@SubscribeEvent
public void onChunkLoad(ChunkDataEvent.Load chunkevt)
{
World w = chunkevt.getWorld();
if (!(w instanceof WorldServer))
{
return;
}
getSemaphoreFor(w);
Chunk chk = chunkevt.getChunk();
Set<String> existingGens = Sets.newHashSet();
NBTTagCompound data = chunkevt.getData();
for (Marker m : markers)
{
NBTTagCompound marker = data.getCompoundTag(m.marker);
NBTTagList tagList = marker.getTagList("list", 8);
for (int i = 0; i < tagList.tagCount(); i++)
{
existingGens.add(tagList.getStringTagAt(i));
}
SetView<String> difference = Sets.difference(m.classes, existingGens);
for (String retro : difference)
{
if (retros.containsKey(retro))
{
queueRetrogen(retro, w, chk.getChunkCoordIntPair());
}
}
}
for (String retro : existingGens)
{
completeRetrogen(chk.getChunkCoordIntPair(), w, retro);
}
}
@SubscribeEvent
public void onChunkSave(ChunkDataEvent.Save chunkevt)
{
World w = chunkevt.getWorld();
if (!(w instanceof WorldServer))
{
return;
}
getSemaphoreFor(w).acquireUninterruptibly();
try
{
if (completedWork.containsKey(w))
{
ListMultimap<ChunkPos, String> doneChunks = completedWork.get(w);
List<String> retroClassList = doneChunks.get(chunkevt.getChunk().getChunkCoordIntPair());
if (retroClassList.isEmpty())
return;
NBTTagCompound data = chunkevt.getData();
for (String retroClass : retroClassList)
{
String marker = retros.get(retroClass);
if (marker == null)
{
FMLLog.log(Level.DEBUG, "Encountered retrogen class %s with no existing marker, removing from chunk. You probably removed it from the active configuration", retroClass);
continue;
}
NBTTagList lst;
if (data.hasKey(marker)) {
lst = data.getCompoundTag(marker).getTagList("list", 8);
} else {
NBTTagCompound retro = new NBTTagCompound();
lst = new NBTTagList();
retro.setTag("list", lst);
data.setTag(marker, retro);
}
lst.appendTag(new NBTTagString(retroClass));
}
}
}
finally
{
getSemaphoreFor(w).release();
}
}
private void queueRetrogen(String retro, World world, ChunkPos chunkCoords)
{
if (world instanceof WorldServer)
{
ListMultimap<ChunkPos, String> currentWork = pendingWork.get(world);
if (currentWork == null)
{
currentWork = ArrayListMultimap.create();
pendingWork.put(world, currentWork);
}
currentWork.put(chunkCoords, retro);
}
}
private void completeRetrogen(ChunkPos chunkCoords, World world, String retroClass)
{
ListMultimap<ChunkPos, String> pendingMap = pendingWork.get(world);
if (pendingMap != null && pendingMap.containsKey(chunkCoords))
{
pendingMap.remove(chunkCoords, retroClass);
}
getSemaphoreFor(world).acquireUninterruptibly();
try
{
ListMultimap<ChunkPos, String> completedMap = completedWork.get(world);
if (completedMap == null)
{
completedMap = ArrayListMultimap.create();
completedWork.put(world, completedMap);
}
completedMap.put(chunkCoords, retroClass);
}
finally
{
getSemaphoreFor(world).release();
}
}
private void runRetrogen(WorldServer world, ChunkPos chunkCoords, String retroClass)
{
long worldSeed = world.getSeed();
Random fmlRandom = new Random(worldSeed);
long xSeed = fmlRandom.nextLong() >> 2 + 1L;
long zSeed = fmlRandom.nextLong() >> 2 + 1L;
long chunkSeed = (xSeed * chunkCoords.chunkXPos + zSeed * chunkCoords.chunkZPos) ^ worldSeed;
fmlRandom.setSeed(chunkSeed);
ChunkProviderServer providerServer = world.getChunkProvider();
IChunkGenerator generator = ObfuscationReflectionHelper.getPrivateValue(ChunkProviderServer.class, providerServer, "field_186029_c", "chunkGenerator");
delegates.get(retroClass).delegate.generate(fmlRandom, chunkCoords.chunkXPos, chunkCoords.chunkZPos, world, generator, providerServer);
FMLLog.fine("Retrogenerated chunk for %s", retroClass);
completeRetrogen(chunkCoords, world, retroClass);
}
}