move core into a folder named "core"

This is so we can have multiple sub-projects in the core repo
This commit is contained in:
James Seibel
2022-08-30 07:36:19 -05:00
parent 519a4c1452
commit 9799b0a263
389 changed files with 0 additions and 0 deletions
@@ -0,0 +1,255 @@
package com.seibel.lod.core;
import com.formdev.flatlaf.FlatDarkLaf;
import com.formdev.flatlaf.FlatLightLaf;
import com.seibel.lod.core.jar.DarkModeDetector;
import com.seibel.lod.core.jar.JarUtils;
import com.seibel.lod.core.jar.gui.BaseJFrame;
import com.seibel.lod.core.jar.gui.cusomJObject.JBox;
import com.seibel.lod.core.jar.installer.ModrinthGetter;
import com.seibel.lod.core.jar.installer.WebDownloader;
import com.seibel.lod.core.jar.JarDependencySetup;
import javax.swing.*;
import java.awt.*;
import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
/**
* The main class when you run the standalone jar
*
* @author coolGi
*/
// Once built it would be in core/build/libs/DistantHorizons-<Version>-dev-all.jar
public class JarMain {
public static final boolean isDarkTheme = DarkModeDetector.isDarkMode();
public static boolean isOffline = WebDownloader.netIsAvailable();
public static void main(String[] args) {
// Sets up the local
if (JarUtils.accessFile("assets/lod/lang/" + Locale.getDefault().toString().toLowerCase() + ".json") == null) {
System.out.println("The language setting [" + Locale.getDefault().toString().toLowerCase() + "] isn't allowed yet. Defaulting to [" + Locale.US.toString().toLowerCase() + "].");
Locale.setDefault(Locale.US);
}
// Set up the theme
if (isDarkTheme)
FlatDarkLaf.setup();
else
FlatLightLaf.setup();
JarDependencySetup.createInitialBindings();
// GitlabGetter.init();
ModrinthGetter.init();
System.out.println("WARNING: The standalone jar still work in progress");
// JOptionPane.showMessageDialog(null, "The GUI for the standalone jar isn't made yet\nIf you want to use the mod then put it in your mods folder", "Distant Horizons", JOptionPane.WARNING_MESSAGE);
// if (getOperatingSystem().equals(OperatingSystem.MACOS)) {
// System.out.println("If you want the installer then please use Linux or Windows for the time being.\nMacOS support/testing will come later on");
// }
// Code will be changed later on to allow resizing and work better
BaseJFrame frame = new BaseJFrame(false, true);
frame.addExtraButtons(frame.getWidth(), 0, true, false);
// Buttons which you want to be stacked vertically should be added with this (`frame.add(obj, this);`)
GridBagConstraints verticalLayout = new GridBagConstraints();
verticalLayout.gridy = GridBagConstraints.RELATIVE;
verticalLayout.gridx = 0;
verticalLayout.fill = GridBagConstraints.HORIZONTAL;
verticalLayout.weightx = 1.0;
verticalLayout.anchor = GridBagConstraints.NORTH;
// Selected download
AtomicReference<String> downloadID = new AtomicReference<String>("");
// This is for the panel to show the update description
JPanel modVersionDescriptionPanel = new JPanel(new GridBagLayout());
JScrollPane modVersionDescriptionScroll = new JScrollPane(modVersionDescriptionPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
// Sets all the layout stuff for it
int modDescriptionWidth = 275;
modVersionDescriptionScroll.setBounds(frame.getWidth()-modDescriptionWidth, 225, modDescriptionWidth, frame.getHeight()-255);
modVersionDescriptionScroll.setBorder(null); // Disables the border
modVersionDescriptionScroll.setWheelScrollingEnabled(true);
// The label
JLabel modVersionDescriptionLabel = new JLabel();
modVersionDescriptionPanel.add(modVersionDescriptionLabel, verticalLayout);
// Finally add it
frame.add(modVersionDescriptionScroll);
// This is for the pannel to select MinecraftVersion
JPanel modMinecraftVersionsPannel = new JPanel(new GridBagLayout());
JScrollPane modMinecraftVersionsScroll = new JScrollPane(modMinecraftVersionsPannel, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
// Sets all the layout stuff for it
modMinecraftVersionsScroll.setBounds(0, 225, 125, frame.getHeight()-255);
modMinecraftVersionsScroll.setBorder(null); // Disables the border
modMinecraftVersionsScroll.setWheelScrollingEnabled(true);
// List to store all the buttons
ArrayList<JButton> modMinecraftReleaseButtons = new ArrayList<>();
frame.add(modMinecraftVersionsScroll);
// This is for selecting the mod version
JPanel modVersionsPannel = new JPanel(new GridBagLayout());
JScrollPane modVersionsScroll = new JScrollPane(modVersionsPannel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
// Sets all the layout stuff for it
modVersionsScroll.setBounds(125, 225, 100, frame.getHeight()-255);
modVersionsScroll.setBorder(null); // Disables the border
modVersionsScroll.setWheelScrollingEnabled(true);
// List to store all the buttons
ArrayList<JButton> modReleaseButtons = new ArrayList<>();
frame.add(modVersionsScroll);
// Add all the buttons
for (String mcVer : ModrinthGetter.mcVersions) {
JButton btn = new JButton(mcVer);
btn.setBackground(UIManager.getColor("Panel.background")); // Does the same thing as removing the background
btn.setBorderPainted(false); // Removes the borders
// btn.setHorizontalAlignment(SwingConstants.LEFT); // Sets the text to be on the left side rather than the center
btn.addActionListener(e -> {
// Clears the selected colors for the rest of the buttons
for (JButton currentBtn : modMinecraftReleaseButtons)
currentBtn.setBackground(UIManager.getColor("Panel.background"));
btn.setBackground(UIManager.getColor("Button.background")); // Sets this to the selected color
// Clears the minecraft version panel
modVersionsPannel.removeAll();
modReleaseButtons.clear();
// Adds all the buttons for the minecraft panel
for (String modID : ModrinthGetter.mcVerToReleaseID.get(mcVer)) {
// No need to comment most of these as it is the same this as before
JButton btnDownload = new JButton(ModrinthGetter.releaseNames.get(modID));
btnDownload.setBackground(UIManager.getColor("Panel.background"));
btnDownload.setBorderPainted(false);
btnDownload.setHorizontalAlignment(SwingConstants.LEFT);
btnDownload.addActionListener(f -> {
downloadID.set(modID);
for (JButton currentBtn : modReleaseButtons)
currentBtn.setBackground(UIManager.getColor("Panel.background"));
btnDownload.setBackground(UIManager.getColor("Button.background"));
modVersionDescriptionLabel.setText(
WebDownloader.formatMarkdownToHtml(
ModrinthGetter.changeLogs.get(modID), modDescriptionWidth-75)
);
modVersionDescriptionPanel.repaint();
});
modVersionsPannel.add(btnDownload, verticalLayout);
modReleaseButtons.add(btnDownload);
}
modVersionsScroll.getVerticalScrollBar().setValue(0); // Reset the scroll bar back to the top
modVersionsPannel.repaint(); // Update the version pannel
frame.validate(); // Update the frame
});
modMinecraftVersionsPannel.add(btn, verticalLayout);
modMinecraftReleaseButtons.add(btn);
}
// Bar at the top
frame.add(new JBox(UIManager.getColor("Separator.foreground"), 0, 220, frame.getWidth(), 5));
// Minecraft version text
JLabel textMcVersionHeader = new JLabel("Minecraft version");
textMcVersionHeader.setBounds(0, 200, 125, 20);
frame.add(textMcVersionHeader);
// Version text
JLabel textVersionHeader = new JLabel("Mod version");
textVersionHeader.setBounds(125, 200, 150, 20);
frame.add(textVersionHeader);
// Stuff for setting the file install path
JFileChooser minecraftDirPop = new JFileChooser();
if (getOperatingSystem().equals(OperatingSystem.WINDOWS))
minecraftDirPop.setCurrentDirectory(new File(System.getenv("APPDATA") + "/.minecraft/mods"));
if (getOperatingSystem().equals(OperatingSystem.LINUX))
minecraftDirPop.setCurrentDirectory(new File(System.getProperty("user.home") + "/.minecraft/mods"));
minecraftDirPop.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
JButton minecraftDirBtn = new JButton("Click to select install path");
minecraftDirBtn.addActionListener(e -> {
if (minecraftDirPop.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION)
minecraftDirBtn.setText(minecraftDirPop.getSelectedFile().toString());
});
minecraftDirBtn.setBounds(230, frame.getHeight()-100, 200, 20);
frame.add(minecraftDirBtn);
// Button for the install button
JButton installMod = new JButton("Install " + ModInfo.READABLE_NAME);
installMod.setBounds(230, frame.getHeight()-70, 200, 20);
installMod.addActionListener(e -> {
if (minecraftDirPop.getSelectedFile() == null) {
JOptionPane.showMessageDialog(frame, "Please select your install directory", ModInfo.READABLE_NAME, JOptionPane.WARNING_MESSAGE);
return;
}
URL downloadPath = ModrinthGetter.downloadUrl.get(downloadID.get());
try {
WebDownloader.downloadAsFile(
downloadPath,
minecraftDirPop.getSelectedFile().toPath().resolve(
ModInfo.NAME + "-" + ModrinthGetter.releaseNames.get(downloadID.get()) + ".jar"
).toFile());
JOptionPane.showMessageDialog(frame, "Installation done. \nYou can now close the installer", ModInfo.READABLE_NAME, JOptionPane.INFORMATION_MESSAGE);
} catch (Exception f) {
JOptionPane.showMessageDialog(frame, "Download failed. Check your internet connection \nStacktrace: " + f.getMessage(), ModInfo.READABLE_NAME, JOptionPane.ERROR_MESSAGE);
}
});
frame.add(installMod);
// Fabric installer
// try {
// WebDownloader.downloadAsFile(new URL("https://maven.fabricmc.net/net/fabricmc/fabric-installer/0.11.0/fabric-installer-0.11.0.jar"), new File(System.getProperty("java.io.tmpdir") + "/fabricInstaller.jar"));
// Runtime.getRuntime().exec("java -jar " + System.getProperty("java.io.tmpdir") + "/fabricInstaller.jar");
// } catch (Exception e) {e.printStackTrace();}
frame.addLogo(); // Has to be run at the end cus of a bug with java swing (it may not be a bug but idk how to fix it so I'll call it a bug)
frame.validate(); // Update to add the widgets
frame.setVisible(true); // Start the ui
}
public enum OperatingSystem {WINDOWS, MACOS, LINUX, NONE} // Easy to use enum for the 3 main os's
public static OperatingSystem getOperatingSystem() { // Get the os and turn it into that enum
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) {
return OperatingSystem.WINDOWS;
} else if (os.contains("mac")) {
return OperatingSystem.MACOS;
} else if (os.contains("nix") || os.contains("nux")) {
return OperatingSystem.LINUX;
} else {
return OperatingSystem.NONE; // If you are the 0.00001% who don't use one of these 3 os's then you get light theme
}
}
}
@@ -0,0 +1,55 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core;
import java.util.Locale;
/**
* This file is similar to mcmod.info
* <br>
* If you are looking at this mod's source code and don't
* know where to start.
* Go to the api/lod package (folder) and take a look at the ClientApi.java file,
* Pretty much all of the mod stems from there.
*
* @author James Seibel
* @author Ran
* @version 2022-4-27
*/
public final class ModInfo
{
public static final String ID = "lod";
/** The internal protocol version used for networking */
public static final int PROTOCOL_VERSION = 1;
/** The internal mod name */
public static final String NAME = "DistantHorizons";
/** Human readable version of NAME */
public static final String READABLE_NAME = "Distant Horizons";
public static final String VERSION = "1.7.0a-dev";
/** Returns true if the current build is an unstable developer build, false otherwise. */
public static boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev");
/** This version should only be updated when breaking changes are introduced to the DH API */
public static final int API_MAJOR_VERSION = 0;
/** This version should be updated whenever new methods are added to the DH API */
public static final int API_MINOR_VERSION = 0;
}
@@ -0,0 +1,13 @@
package com.seibel.lod.core.a7;
import com.seibel.lod.core.a7.datatype.column.ColumnRenderLoader;
import com.seibel.lod.core.a7.datatype.full.FullDataLoader;
import com.seibel.lod.core.a7.datatype.full.SparseDataLoader;
public class Initializer {
public static void init() {
ColumnRenderLoader unused = new ColumnRenderLoader(); // Auto register into the loader system
FullDataLoader unused2 = new FullDataLoader(); // Auto register into the loader system
SparseDataLoader unused3 = new SparseDataLoader(); // Auto register
}
}
@@ -0,0 +1,58 @@
package com.seibel.lod.core.a7.datatype;
import com.google.common.collect.HashMultimap;
import com.seibel.lod.core.a7.level.ILevel;
import com.seibel.lod.core.a7.save.io.file.DataMetaFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
public abstract class DataSourceLoader {
public static final HashMultimap<Class<? extends LodDataSource>, DataSourceLoader> loaderRegistry = HashMultimap.create();
public final Class<? extends LodDataSource> clazz;
public static final HashMap<Long, Class<? extends LodDataSource>> datatypeIdRegistry = new HashMap<>();
public static DataSourceLoader getLoader(long dataTypeId, byte dataVersion) {
return loaderRegistry.get(datatypeIdRegistry.get(dataTypeId)).stream()
.filter(l -> Arrays.binarySearch(l.loaderSupportedVersions, dataVersion) >= 0)
.findFirst().orElse(null);
}
public static DataSourceLoader getLoader(Class<? extends LodDataSource> clazz, byte dataVersion) {
return loaderRegistry.get(clazz).stream()
.filter(l -> Arrays.binarySearch(l.loaderSupportedVersions, dataVersion) >= 0)
.findFirst().orElse(null);
}
public final long datatypeId;
public final byte[] loaderSupportedVersions;
public DataSourceLoader(Class<? extends LodDataSource> clazz, long datatypeId, byte[] loaderSupportedVersions) {
this.datatypeId = datatypeId;
this.loaderSupportedVersions = loaderSupportedVersions;
Arrays.sort(loaderSupportedVersions); // sort to allow fast access
this.clazz = clazz;
if (datatypeIdRegistry.containsKey(datatypeId) && datatypeIdRegistry.get(datatypeId) != clazz) {
throw new IllegalArgumentException("Loader for datatypeId " + datatypeId + " already registered with different class: "
+ datatypeIdRegistry.get(datatypeId) + " != " + clazz);
}
Set<DataSourceLoader> loaders = loaderRegistry.get(clazz);
if (loaders.stream().anyMatch(other -> {
// see if any loaderSupportsVersion conflicts with this one
for (byte otherVer : other.loaderSupportedVersions) {
if (Arrays.binarySearch(loaderSupportedVersions, otherVer) >= 0) return true;
}
return false;
})) {
throw new IllegalArgumentException("Loader for class " + clazz + " that supports one of the version in "
+ Arrays.toString(loaderSupportedVersions) + " already registered!");
}
datatypeIdRegistry.put(datatypeId, clazz);
loaderRegistry.put(clazz, this);
}
// Can return null as meaning the requirement is not met
public abstract LodDataSource loadData(DataMetaFile dataFile, InputStream data, ILevel level) throws IOException;
}
@@ -0,0 +1,80 @@
package com.seibel.lod.core.a7.datatype;
import java.util.concurrent.*;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.datatype.transform.LodDataBuilder;
import com.seibel.lod.core.a7.level.ILevel;
import com.seibel.lod.core.config.Config;
import com.seibel.lod.core.logging.ConfigBasedLogger;
import com.seibel.lod.core.objects.DHChunkPos;
import com.seibel.lod.core.util.*;
import com.seibel.lod.core.wrapperInterfaces.chunk.IChunkWrapper;
import org.apache.logging.log4j.LogManager;
//FIXME: Unused class???
public class LodBuilder {
public static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Debugging.DebugSwitch.logLodBuilderEvent.get());
static class Task {
final DHChunkPos chunkPos;
final CompletableFuture<ChunkSizedData> future;
Task(DHChunkPos chunkPos, CompletableFuture<ChunkSizedData> future) {
this.chunkPos = chunkPos;
this.future = future;
}
}
private final ConcurrentHashMap<DHChunkPos, IChunkWrapper> latestChunkToBuild = new ConcurrentHashMap<>();
private final ConcurrentLinkedDeque<Task> taskToBuild = new ConcurrentLinkedDeque<>();
private final ExecutorService executor = LodUtil.makeSingleThreadPool(LodBuilder.class);
private final EventLoop ticker = new EventLoop(executor, this::_tick);
ILevel level;
public LodBuilder(ILevel level) {
this.level = level;
}
public CompletableFuture<ChunkSizedData> tryGenerateData(IChunkWrapper chunk) {
if (chunk == null) throw new NullPointerException("ChunkWrapper cannot be null!");
IChunkWrapper oldChunk = latestChunkToBuild.put(chunk.getChunkPos(), chunk); // an Exchange operation
// If there's old chunk, that means we just replaced an unprocessed old request on generating data on this pos.
// if so, we can just return null to signal this, as the old request's future will instead be the proper one
// that will return the latest generated data.
if (oldChunk != null) return null;
// Otherwise, it means we're the first to do so. Lets submit our task to this entry.
CompletableFuture<ChunkSizedData> future = new CompletableFuture<>();
taskToBuild.addLast(new Task(chunk.getChunkPos(), future));
return future;
}
public void tick() {
ticker.tick();
}
private void _tick() {
Task task = taskToBuild.pollFirst();
if (task == null) return; // There's no jobs.
IChunkWrapper latestChunk = latestChunkToBuild.remove(task.chunkPos); // Basically an Exchange operation
if (latestChunk == null) {
LOGGER.error("Somehow Task at {} has latestChunk as null! Skipping task!", task.chunkPos);
task.future.complete(null);
return;
}
if (LodDataBuilder.canGenerateLodFromChunk(latestChunk)) {
ChunkSizedData data = LodDataBuilder.createChunkData(latestChunk);
if (data != null) {
task.future.complete(data);
return;
}
}
// Failed to build due to chunk not meeting requirement.
IChunkWrapper casChunk = latestChunkToBuild.putIfAbsent(task.chunkPos, latestChunk); // CAS operation with expected=null
if (casChunk == null) // That means CAS have been successful
taskToBuild.addLast(task); // Then add back the same old task.
else // Else, it means someone managed to sneak in a new gen request in this pos. Then lets drop this old task.
task.future.complete(null);
}
}
@@ -0,0 +1,24 @@
package com.seibel.lod.core.a7.datatype;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.level.ILevel;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.a7.save.io.file.DataMetaFile;
import com.seibel.lod.core.a7.util.IOUtil;
import com.seibel.lod.core.objects.DHChunkPos;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
public interface LodDataSource {
DhSectionPos getSectionPos();
byte getDataDetail();
void setLocalVersion(int localVer);
byte getDataVersion();
void update(ChunkSizedData data);
// Saving related
void saveData(ILevel level, DataMetaFile file, OutputStream dataStream) throws IOException;
}
@@ -0,0 +1,44 @@
package com.seibel.lod.core.a7.datatype;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.level.IClientLevel;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.a7.render.LodQuadTree;
import com.seibel.lod.core.a7.render.RenderBuffer;
import com.seibel.lod.core.a7.save.io.render.RenderMetaFile;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicReference;
public interface LodRenderSource {
DhSectionPos getSectionPos();
void enableRender(IClientLevel level, LodQuadTree quadTree);
void disableRender();
boolean isRenderReady();
void dispose(); // notify the container that the parent lodSection is now disposed (can be in loaded or unloaded state)
byte getDetailOffset();
/**
* Try and swap in new render buffer for this section. Note that before this call, there should be no other
* places storing or referencing the render buffer.
* @param referenceSlotsOpaque The opaque slot for swapping in the new buffer.
* @param referenceSlotsTransparent The transparent slot for swapping in the new buffer.
* @return True if the swap was successful. False if swap is not needed or if it is in progress.
*/
boolean trySwapRenderBuffer(LodQuadTree quadTree, AtomicReference<RenderBuffer> referenceSlotsOpaque, AtomicReference<RenderBuffer> referenceSlotsTransparent);
void saveRender(IClientLevel level, RenderMetaFile file, OutputStream dataStream) throws IOException;
void write(ChunkSizedData chunkData);
void flushWrites(IClientLevel level);
byte getRenderVersion();
/**
* Whether this object is still valid. If not, a new one should be created.
*/
boolean isValid();
}
@@ -0,0 +1,72 @@
package com.seibel.lod.core.a7.datatype;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.level.IClientLevel;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.a7.render.LodQuadTree;
import com.seibel.lod.core.a7.render.RenderBuffer;
import com.seibel.lod.core.a7.save.io.render.RenderMetaFile;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicReference;
public class PlaceHolderRenderSource implements LodRenderSource {
final DhSectionPos pos;
boolean isValid = true;
public PlaceHolderRenderSource(DhSectionPos pos) {
this.pos = pos;
}
@Override
public DhSectionPos getSectionPos() {
return pos;
}
@Override
public void enableRender(IClientLevel level, LodQuadTree quadTree) {
}
@Override
public void disableRender() {}
@Override
public boolean isRenderReady() {
return false;
}
@Override
public void dispose() {}
@Override
public byte getDetailOffset() {
return 0;
}
@Override
public boolean trySwapRenderBuffer(LodQuadTree quadTree, AtomicReference<RenderBuffer> referenceSlotsOpaque, AtomicReference<RenderBuffer> referenceSlotsTransparent) {
return false;
}
@Override
public void saveRender(IClientLevel level, RenderMetaFile file, OutputStream dataStream) throws IOException {
throw new UnsupportedOperationException("EmptyRenderSource should NEVER be saved!");
}
@Override
public void write(ChunkSizedData chunkData) {}
@Override
public void flushWrites(IClientLevel level) {}
@Override
public byte getRenderVersion() {
return 0;
}
public void markInvalid() {
isValid = false;
}
@Override
public boolean isValid() {
return isValid;
}
}
@@ -0,0 +1,60 @@
package com.seibel.lod.core.a7.datatype;
import com.google.common.collect.HashMultimap;
import com.seibel.lod.core.a7.level.IClientLevel;
import com.seibel.lod.core.a7.save.io.render.RenderMetaFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
public abstract class RenderSourceLoader {
public static final HashMultimap<Class<? extends LodRenderSource>, RenderSourceLoader> loaderRegistry = HashMultimap.create();
public static final HashMap<Long, Class<? extends LodRenderSource>> renderTypeIdRegistry = new HashMap<>();
public static RenderSourceLoader getLoader(long renderTypeId, byte loaderVersion) {
return loaderRegistry.get(renderTypeIdRegistry.get(renderTypeId)).stream()
.filter(l -> Arrays.binarySearch(l.loaderSupportedVersions, loaderVersion) >= 0)
.findFirst().orElse(null);
}
public static RenderSourceLoader getLoader(Class<? extends LodRenderSource> clazz, byte loaderVersion) {
return loaderRegistry.get(clazz).stream()
.filter(l -> Arrays.binarySearch(l.loaderSupportedVersions, loaderVersion) >= 0)
.findFirst().orElse(null);
}
public final Class<? extends LodRenderSource> clazz;
public final long renderTypeId;
public final byte[] loaderSupportedVersions;
public final byte detailOffset;
public RenderSourceLoader(Class<? extends LodRenderSource> clazz, long renderTypeId, byte[] loaderSupportedVersions, byte detailOffset) {
this.renderTypeId = renderTypeId;
this.loaderSupportedVersions = loaderSupportedVersions;
Arrays.sort(loaderSupportedVersions); // sort to allow fast access
this.clazz = clazz;
if (renderTypeIdRegistry.containsKey(renderTypeId) && renderTypeIdRegistry.get(renderTypeId) != clazz) {
throw new IllegalArgumentException("Loader for renderTypeId " + renderTypeId + " already registered with different class: "
+ renderTypeIdRegistry.get(renderTypeId) + " != " + clazz);
}
Set<RenderSourceLoader> loaders = loaderRegistry.get(clazz);
if (loaders.stream().anyMatch(other -> {
// see if any loaderSupportsVersion conflicts with this one
for (byte otherVer : other.loaderSupportedVersions) {
if (Arrays.binarySearch(loaderSupportedVersions, otherVer) >= 0) return true;
}
return false;
})) {
throw new IllegalArgumentException("Loader for class " + clazz + " that supports one of the version in "
+ Arrays.toString(loaderSupportedVersions) + " already registered!");
}
renderTypeIdRegistry.put(renderTypeId, clazz);
loaderRegistry.put(clazz, this);
this.detailOffset = detailOffset;
}
// Can return null as meaning the file is out of date or something
public abstract LodRenderSource loadRender(RenderMetaFile renderFile, InputStream data, IClientLevel level) throws IOException;
public abstract LodRenderSource createRender(LodDataSource dataSource, IClientLevel level);
}
@@ -0,0 +1,39 @@
package com.seibel.lod.core.a7.datatype.column;
import com.seibel.lod.core.a7.datatype.LodDataSource;
import com.seibel.lod.core.a7.datatype.full.FullDataSource;
import com.seibel.lod.core.a7.datatype.transform.FullToColumnTransformer;
import com.seibel.lod.core.a7.level.IClientLevel;
import com.seibel.lod.core.a7.datatype.LodRenderSource;
import com.seibel.lod.core.a7.datatype.RenderSourceLoader;
import com.seibel.lod.core.a7.save.io.render.RenderMetaFile;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
public class ColumnRenderLoader extends RenderSourceLoader {
public ColumnRenderLoader() {
super(ColumnRenderSource.class, ColumnRenderSource.TYPE_ID, new byte[]{ColumnRenderSource.LATEST_VERSION}, ColumnRenderSource.SECTION_SIZE_OFFSET);
}
@Override
public LodRenderSource loadRender(RenderMetaFile dataFile, InputStream data, IClientLevel level) throws IOException {
try (
//TODO: Add decompressor here
DataInputStream dis = new DataInputStream(data);
) {
return new ColumnRenderSource(dataFile.pos, dis, dataFile.loaderVersion, level);
}
}
@Override
public LodRenderSource createRender(LodDataSource dataSource, IClientLevel level) {
if (dataSource instanceof FullDataSource) {
return FullToColumnTransformer.transformFullDataToColumnData(level, (FullDataSource) dataSource);
}
return null;
}
}
@@ -0,0 +1,405 @@
package com.seibel.lod.core.a7.datatype.column;
import com.seibel.lod.core.a7.datatype.column.accessor.ColumnArrayView;
import com.seibel.lod.core.a7.datatype.column.accessor.ColumnFormat;
import com.seibel.lod.core.a7.datatype.column.accessor.ColumnQuadView;
import com.seibel.lod.core.a7.datatype.column.accessor.IColumnDatatype;
import com.seibel.lod.core.a7.datatype.column.render.ColumnRenderBuffer;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.datatype.transform.FullToColumnTransformer;
import com.seibel.lod.core.a7.level.IClientLevel;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.a7.render.RenderBuffer;
import com.seibel.lod.core.a7.render.a7LodRenderer;
import com.seibel.lod.core.a7.save.io.render.RenderMetaFile;
import com.seibel.lod.core.enums.ELodDirection;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.objects.LodDataView;
import com.seibel.lod.core.a7.level.ILevel;
import com.seibel.lod.core.a7.render.LodQuadTree;
import com.seibel.lod.core.a7.render.LodRenderSection;
import com.seibel.lod.core.a7.datatype.LodRenderSource;
import com.seibel.lod.core.util.Reference;
import com.seibel.lod.core.util.LodUtil;
import org.apache.logging.log4j.Logger;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicReference;
public class ColumnRenderSource implements LodRenderSource, IColumnDatatype {
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public static final boolean DO_SAFETY_CHECKS = true;
public static final byte SECTION_SIZE_OFFSET = 6;
public static final int SECTION_SIZE = 1 << SECTION_SIZE_OFFSET;
public static final byte LATEST_VERSION = 1;
public static final long TYPE_ID = "ColumnRenderSource".hashCode();
public static final int AIR_LODS_SIZE = 16;
public static final int AIR_SECTION_SIZE = SECTION_SIZE/AIR_LODS_SIZE;
public final int verticalSize;
public final DhSectionPos sectionPos;
public final int yOffset;
public final long[] dataContainer;
public final int[] airDataContainer;
public boolean isEmpty = true;
/**
* Constructor of the ColumnDataType
* @param maxVerticalSize the maximum vertical size of the container
*/
public ColumnRenderSource(DhSectionPos sectionPos, int maxVerticalSize, int yOffset) {
verticalSize = maxVerticalSize;
dataContainer = new long[SECTION_SIZE * SECTION_SIZE * verticalSize];
airDataContainer = new int[AIR_SECTION_SIZE * AIR_SECTION_SIZE * verticalSize];
this.sectionPos = sectionPos;
this.yOffset = yOffset;
}
private long[] loadData(DataInputStream inputData, int version, int verticalSize) throws IOException {
switch (version) {
case 1:
return readDataV1(inputData, verticalSize);
default:
throw new IOException("Invalid Data: The version of the data is not supported");
}
}
private long[] readDataV1(DataInputStream inputData, int tempMaxVerticalData) throws IOException {
int x = SECTION_SIZE * SECTION_SIZE * tempMaxVerticalData;
short tempMinHeight = Short.reverseBytes(inputData.readShort());
if (tempMinHeight == Short.MAX_VALUE) { //FIXME: Temp hack flag for marking a empty section
return new long[x];
}
isEmpty = false;
byte[] data = new byte[x * Long.BYTES];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
inputData.readFully(data);
long[] result = new long[x];
bb.asLongBuffer().get(result);
if (tempMinHeight != yOffset) {
for (int i=0; i<result.length; i++) {
result[i] = ColumnFormat.shiftHeightAndDepth(result[i], (short) (tempMinHeight - yOffset));
}
}
return result;
}
// Load from data stream with maxVerticalSize loaded from the data stream
public ColumnRenderSource(DhSectionPos sectionPos, DataInputStream inputData, int version, ILevel level) throws IOException {
this.sectionPos = sectionPos;
yOffset = level.getMinY();
byte detailLevel = inputData.readByte();
if (sectionPos.sectionDetail - SECTION_SIZE_OFFSET != detailLevel) {
throw new IOException("Invalid data: detail level does not match");
}
verticalSize = inputData.readByte() & 0b01111111;
dataContainer = loadData(inputData, version, verticalSize);
airDataContainer = new int[AIR_SECTION_SIZE * AIR_SECTION_SIZE * verticalSize];
}
@Override
public void clear(int posX, int posZ)
{
for (int verticalIndex = 0; verticalIndex < verticalSize; verticalIndex++)
dataContainer[posX * SECTION_SIZE * verticalSize + posZ * verticalSize + verticalIndex] =
ColumnFormat.EMPTY_DATA;
}
@Override
public boolean addData(long data, int posX, int posZ, int verticalIndex)
{
dataContainer[posX * SECTION_SIZE * verticalSize + posZ * verticalSize + verticalIndex] = data;
return true;
}
@Override
public boolean copyVerticalData(LodDataView data, int posX, int posZ, boolean override) {
if (DO_SAFETY_CHECKS) {
if (data.size() != verticalSize)
throw new IllegalArgumentException("data size not the same as vertical size");
if (posX < 0 || posX >= SECTION_SIZE)
throw new IllegalArgumentException("X position is out of bounds");
if (posZ < 0 || posZ >= SECTION_SIZE)
throw new IllegalArgumentException("Z position is out of bounds");
}
int index = posX * SECTION_SIZE * verticalSize + posZ * verticalSize;
int compare = ColumnFormat.compareDatapointPriority(data.get(0), dataContainer[index]);
if (override) {
if (compare<0) return false;
} else {
if (compare<=0) return false;
}
data.copyTo(dataContainer, index);
return true;
}
@Override
public long getData(int posX, int posZ, int verticalIndex)
{
return dataContainer[posX * SECTION_SIZE * verticalSize + posZ * verticalSize + verticalIndex];
}
@Override
public long[] getAllData(int posX, int posZ)
{
long[] result = new long[verticalSize];
int index = posX * SECTION_SIZE * verticalSize + posZ * verticalSize;
System.arraycopy(dataContainer, index, result, 0, verticalSize);
return result;
}
@Override
public ColumnArrayView getVerticalDataView(int posX, int posZ) {
return new ColumnArrayView(dataContainer, verticalSize,
posX * SECTION_SIZE * verticalSize + posZ * verticalSize, verticalSize);
}
@Override
public ColumnQuadView getDataInQuad(int quadX, int quadZ, int quadXSize, int quadZSize) {
return new ColumnQuadView(dataContainer, SECTION_SIZE, verticalSize, quadX, quadZ, quadXSize, quadZSize);
}
@Override
public ColumnQuadView getFullQuad() {
return new ColumnQuadView(dataContainer, SECTION_SIZE, verticalSize, 0, 0, SECTION_SIZE, SECTION_SIZE);
}
@Override
public int getVerticalSize()
{
return verticalSize;
}
@Override
public boolean doesItExist(int posX, int posZ)
{
return ColumnFormat.doesItExist(getSingleData(posX, posZ));
}
@Override
public void generateData(IColumnDatatype lowerDataContainer, int posX, int posZ)
{
ColumnQuadView quadView = lowerDataContainer.getDataInQuad(posX*2, posZ*2, 2,2);
ColumnArrayView outputView = getVerticalDataView(posX, posZ);
outputView.mergeMultiDataFrom(quadView);
}
boolean writeData(DataOutputStream output) throws IOException {
output.writeByte(getDataDetail());
output.writeByte((byte) verticalSize);
// FIXME: yOffset is a int, but we only are writing a short.
if (isEmpty) {
output.writeByte(Short.MAX_VALUE & 0xFF);
output.writeByte((Short.MAX_VALUE >> 8) & 0xFF);
return false;
}
output.writeByte((byte) (yOffset & 0xFF));
output.writeByte((byte) ((yOffset >> 8) & 0xFF));
boolean allGenerated = true;
int x = SECTION_SIZE * SECTION_SIZE;
for (int i = 0; i < x; i++)
{
for (int j = 0; j < verticalSize; j++)
{
long current = dataContainer[i * verticalSize + j];
output.writeLong(Long.reverseBytes(current));
}
if (!ColumnFormat.doesItExist(dataContainer[i]))
allGenerated = false;
}
return allGenerated;
}
public String toString()
{
String LINE_DELIMITER = "\n";
String DATA_DELIMITER = " ";
String SUBDATA_DELIMITER = ",";
StringBuilder stringBuilder = new StringBuilder();
int size = sectionPos.getWidth().value;
stringBuilder.append(sectionPos);
stringBuilder.append(LINE_DELIMITER);
for (int z = 0; z < size; z++)
{
for (int x = 0; x < size; x++)
{
for (int y = 0; y < verticalSize; y++) {
//Converting the dataToHex
stringBuilder.append(Long.toHexString(getData(x,z,y)));
if (y != verticalSize-1) stringBuilder.append(SUBDATA_DELIMITER);
}
if (x != size-1) stringBuilder.append(DATA_DELIMITER);
}
if (z != size-1) stringBuilder.append(LINE_DELIMITER);
}
return stringBuilder.toString();
}
@Override
public int getMaxNumberOfLods()
{
return SECTION_SIZE * SECTION_SIZE * getVerticalSize();
}
@Override
public long getRoughRamUsage()
{
return (long) dataContainer.length * Long.BYTES;
}
public DhSectionPos getSectionPos() {
return sectionPos;
}
public byte getDataDetail() {
return (byte) (sectionPos.sectionDetail - SECTION_SIZE_OFFSET);
}
@Override
public byte getDetailOffset() {
return SECTION_SIZE_OFFSET;
}
private CompletableFuture<ColumnRenderBuffer[]> inBuildRenderBuffer = null;
private Reference<ColumnRenderBuffer> usedBufferOpaque = new Reference<>();
private Reference<ColumnRenderBuffer> usedBufferTransparent = new Reference<>();
private void tryBuildBuffer(IClientLevel level, LodQuadTree quadTree) {
if (inBuildRenderBuffer == null && !ColumnRenderBuffer.isBusy() && !isEmpty) {
ColumnRenderSource[] data = new ColumnRenderSource[ELodDirection.ADJ_DIRECTIONS.length];
for (ELodDirection direction : ELodDirection.ADJ_DIRECTIONS) {
LodRenderSection section = quadTree.getSection(sectionPos.getAdjacent(direction)); //FIXME: Handle traveling through different detail levels
if (section != null && section.getRenderContainer() != null && section.getRenderContainer() instanceof ColumnRenderBuffer) {
data[direction.ordinal()-2] = ((ColumnRenderSource) section.getRenderContainer());
}
}
inBuildRenderBuffer = ColumnRenderBuffer.build(level, usedBufferOpaque, usedBufferTransparent, this, data);
}
}
private void cancelBuildBuffer() {
if (inBuildRenderBuffer != null) {
//LOGGER.info("Cancelling build of render buffer for {}", sectionPos);
inBuildRenderBuffer.cancel(true);
inBuildRenderBuffer = null;
}
}
private IClientLevel level = null; //FIXME: hack to pass level into tryBuildBuffer
@Override
public void enableRender(IClientLevel level, LodQuadTree quadTree) {
this.level = level;
//tryBuildBuffer(level, quadTree);
}
@Override
public void disableRender() {
cancelBuildBuffer();
}
@Override
public boolean isRenderReady() {
return inBuildRenderBuffer == null || inBuildRenderBuffer.isDone();
}
@Override
public void dispose() {
cancelBuildBuffer();
}
//FIXME: Temp Hack
private long lastNs = -1;
private static final long SWAP_TIMEOUT = /* 10 sec */ 10_000_000_000L;
private static final long SWAP_BUSY_COLLISION_TIMEOUT = /* 1 sec */ 1_000_000_000L;
@Override
public boolean trySwapRenderBuffer(LodQuadTree quadTree, AtomicReference<RenderBuffer> referenceSlotsOpaque, AtomicReference<RenderBuffer> referenceSlotsTransparent) {
if (lastNs != -1 && System.nanoTime() - lastNs < SWAP_TIMEOUT) {
return false;
}
if (inBuildRenderBuffer != null) {
if (inBuildRenderBuffer.isDone()) {
lastNs = System.nanoTime();
//LOGGER.info("Swapping render buffer for {}", sectionPos);
RenderBuffer[] newBuffers = inBuildRenderBuffer.join();
RenderBuffer oldBuffersOpaque = referenceSlotsOpaque.getAndSet(newBuffers[0]);
ColumnRenderBuffer swapped;
if (oldBuffersOpaque instanceof ColumnRenderBuffer) {
swapped = usedBufferOpaque.swap((ColumnRenderBuffer) oldBuffersOpaque);
LodUtil.assertTrue(swapped == null);
}
if(a7LodRenderer.transparencyEnabled) {
RenderBuffer oldBuffersTransparent = referenceSlotsTransparent.getAndSet(newBuffers[1]);
if (a7LodRenderer.transparencyEnabled) {
if (oldBuffersTransparent instanceof ColumnRenderBuffer) {
swapped = usedBufferTransparent.swap((ColumnRenderBuffer) oldBuffersTransparent);
LodUtil.assertTrue(swapped == null);
}
}
}
inBuildRenderBuffer = null;
return true;
}
} else {
if (!isEmpty) {
if (ColumnRenderBuffer.isBusy()) {
lastNs += (long) (SWAP_BUSY_COLLISION_TIMEOUT * Math.random());
} else tryBuildBuffer(level, quadTree);
}
}
return false;
}
@Override
public void saveRender(IClientLevel level, RenderMetaFile file, OutputStream dataStream) throws IOException {
flushWrites(level);
try (DataOutputStream dos = new DataOutputStream(dataStream)) {
writeData(dos);
}
}
private final ConcurrentLinkedQueue<ChunkSizedData> writeRequest = new ConcurrentLinkedQueue<>();
@Override
public void write(ChunkSizedData chunkData) {
writeRequest.add(chunkData);
}
@Override
public void flushWrites(IClientLevel level) {
boolean didSomething = false;
while (!writeRequest.isEmpty()) {
isEmpty = false;
ChunkSizedData chunkData = writeRequest.poll();
FullToColumnTransformer.writeFullDataChunkToColumnData(this, level, chunkData);
didSomething = true;
}
if (didSomething) {
lastNs = -1; // Reset the timeout to allow rebuilding the buffer again
}
}
@Override
public byte getRenderVersion() {
return LATEST_VERSION;
}
@Override
public boolean isValid() {
return true;
}
}
@@ -0,0 +1,132 @@
package com.seibel.lod.core.a7.datatype.column.accessor;
import java.util.Arrays;
public final class ColumnArrayView implements IColumnDataView {
final long[] data;
final int size; // size in longs
final int offset; // offset in longs
final int vertSize; // vertical size in longs
public ColumnArrayView(long[] data, int size, int offset, int vertSize) {
this.data = data;
this.size = size;
this.offset = offset;
this.vertSize = vertSize;
}
@Override
public long get(int index) {
return data[index + offset];
}
public void set(int index, long value) {
data[index + offset] = value;
}
@Override
public int size() {
return size;
}
@Override
public int verticalSize() {
return vertSize;
}
@Override
public int dataCount() {
return size / vertSize;
}
@Override
public ColumnArrayView subView(int dataIndexStart, int dataCount) {
return new ColumnArrayView(data, dataCount * vertSize, offset + dataIndexStart * vertSize, vertSize);
}
public void fill(long value) {
Arrays.fill(data, offset, offset + size, value);
}
public void copyFrom(IColumnDataView source, int outputDataIndexOffset) {
if (source.verticalSize() > vertSize) throw new IllegalArgumentException("source verticalSize must be <= self's verticalSize to copy");
if (source.dataCount() + outputDataIndexOffset > dataCount()) throw new IllegalArgumentException("dataIndexStart + source.dataCount() must be <= self.dataCount() to copy");
if (source.verticalSize() != vertSize) {
for (int i = 0; i < source.dataCount(); i++) {
int outputOffset = offset + outputDataIndexOffset * vertSize + i * vertSize;
source.subView(i, 1).copyTo(data, outputOffset, source.verticalSize());
Arrays.fill(data, outputOffset + source.verticalSize(),
outputOffset + vertSize, 0);
}
} else {
source.copyTo(data, offset + outputDataIndexOffset * vertSize, source.size());
}
}
public void copyFrom(IColumnDataView source) {
copyFrom(source, 0);
}
@Override
public void copyTo(long[] target, int offset, int size) {
System.arraycopy(data, this.offset, target, offset, size);
}
public boolean mergeWith(ColumnArrayView source, boolean override) {
if (size != source.size) {
throw new IllegalArgumentException("Cannot merge views of different sizes");
}
if (vertSize != source.vertSize) {
throw new IllegalArgumentException("Cannot merge views of different vertical sizes");
}
boolean anyChange = false;
for (int o=0; o<(source.size()*vertSize); o+=vertSize) {
if (override) {
if (ColumnFormat.compareDatapointPriority(source.get(o), get(o)) >= 0) {
anyChange = true;
System.arraycopy(source.data, source.offset+o, data, offset+o, vertSize);
}
} else {
if (ColumnFormat.compareDatapointPriority(source.get(o), get(o)) > 0) {
anyChange = true;
System.arraycopy(source.data, source.offset+o, data, offset+o, vertSize);
}
}
}
return anyChange;
}
public void changeVerticalSizeFrom(IColumnDataView source) {
if (dataCount() != source.dataCount()) {
throw new IllegalArgumentException("Cannot copy and resize to views with different dataCounts");
}
if (vertSize >= source.verticalSize()) {
copyFrom(source);
} else {
for (int i=0; i<dataCount(); i++) {
ColumnFormat.mergeMultiData(source.subView(i, 1), subView(i, 1));
}
}
}
public void mergeMultiDataFrom(IColumnDataView source) {
if (dataCount() != 1) {
throw new IllegalArgumentException("output dataCount must be 1");
}
ColumnFormat.mergeMultiData(source, this);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("S:");
sb.append(size);
sb.append(" V:");
sb.append(vertSize);
sb.append(" O:");
sb.append(offset);
sb.append(" [");
for (int i=0; i<size; i++) {
sb.append(ColumnFormat.toString(data[offset+i]));
if (i < size-1) sb.append(",\n");
}
sb.append("]");
return sb.toString();
}
}
@@ -0,0 +1,570 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.a7.datatype.column.accessor;
import com.seibel.lod.core.a7.datatype.column.accessor.ColumnArrayView;
import com.seibel.lod.core.a7.datatype.column.accessor.IColumnDataView;
import com.seibel.lod.core.logging.SpamReducedLogger;
import com.seibel.lod.core.util.ColorUtil;
import com.seibel.lod.core.util.DataPointUtil;
import com.seibel.lod.core.util.LodUtil;
import java.util.Arrays;
/**
* @author Leonardo Amato
* @version ??
*/
public class ColumnFormat {
/*
|_ |g |g |g |a |a |a |a |
|r |r |r |r |r |r |r |r |
|g |g |g |g |g |g |g |g |
|b |b |b |b |b |b |b |b |
|h |h |h |h |h |h |h |h |
|h |h |h |h |d |d |d |d |
|d |d |d |d |d |d |d |d |
|bl |bl |bl |bl |sl |sl |sl |sl |
*/
// Reminder: bytes have range of [-128, 127].
// When converting to or from an int a 128 should be added or removed.
// If there is a bug with color then it's probably caused by this.
public final static int EMPTY_DATA = 0;
public final static int MAX_WORLD_Y_SIZE = 4096;
public final static int ALPHA_DOWNSIZE_SHIFT = 4;
public final static int GEN_TYPE_SHIFT = 60;
public final static int COLOR_SHIFT = 32;
public final static int BLUE_SHIFT = COLOR_SHIFT;
public final static int GREEN_SHIFT = BLUE_SHIFT + 8;
public final static int RED_SHIFT = GREEN_SHIFT + 8;
public final static int ALPHA_SHIFT = RED_SHIFT + 8;
public final static int HEIGHT_SHIFT = 20;
public final static int DEPTH_SHIFT = 8;
public final static int BLOCK_LIGHT_SHIFT = 4;
public final static int SKY_LIGHT_SHIFT = 0;
public final static long ALPHA_MASK = 0xF;
public final static long RED_MASK = 0xFF;
public final static long GREEN_MASK = 0xFF;
public final static long BLUE_MASK = 0xFF;
public final static long COLOR_MASK = 0xFFFFFF;
public final static long HEIGHT_MASK = 0xFFF;
public final static long DEPTH_MASK = 0xFFF;
public final static long HEIGHT_DEPTH_MASK = 0xFFFFFF;
public final static long BLOCK_LIGHT_MASK = 0xF;
public final static long SKY_LIGHT_MASK = 0xF;
public final static long GEN_TYPE_MASK = 0b111;
public final static long COMPARE_SHIFT = GEN_TYPE_SHIFT;
public final static long HEIGHT_SHIFTED_MASK = HEIGHT_MASK << HEIGHT_SHIFT;
public final static long DEPTH_SHIFTED_MASK = DEPTH_MASK << DEPTH_SHIFT;
public final static long VOID_SETTER = HEIGHT_SHIFTED_MASK | DEPTH_SHIFTED_MASK;
public static long createVoidDataPoint(byte generationMode) {
if (generationMode == 0)
throw new IllegalArgumentException("Trying to create void datapoint with genMode 0, which is NOT allowed in DataPoint version 10!");
return (generationMode & GEN_TYPE_MASK) << GEN_TYPE_SHIFT;
}
public static long createDataPoint(int height, int depth, int color, int lightSky, int lightBlock, int generationMode) {
return createDataPoint(
ColorUtil.getAlpha(color),
ColorUtil.getRed(color),
ColorUtil.getGreen(color),
ColorUtil.getBlue(color),
height, depth, lightSky, lightBlock, generationMode);
}
public static long createDataPoint(int height, int depth, int color, int light, int generationMode) {
LodUtil.assertTrue(light >= 0 && light < 256, "Raw Light value must be between 0 and 255!");
return createDataPoint(
ColorUtil.getAlpha(color),
ColorUtil.getRed(color),
ColorUtil.getGreen(color),
ColorUtil.getBlue(color),
height, depth, light % 16, light / 16, generationMode);
}
public static long createDataPoint(int alpha, int red, int green, int blue, int height, int depth, int lightSky, int lightBlock, int generationMode) {
LodUtil.assertTrue(generationMode != 0, "Trying to create datapoint with genMode 0, which is NOT allowed in DataPoint version 10!");
LodUtil.assertTrue(height >= 0 && height < MAX_WORLD_Y_SIZE, "Trying to create datapoint with height[{}] out of range!", height);
LodUtil.assertTrue(depth >= 0 && depth < MAX_WORLD_Y_SIZE, "Trying to create datapoint with depth[{}] out of range!", depth);
LodUtil.assertTrue(lightSky >= 0 && lightSky < 16, "Trying to create datapoint with lightSky[{}] out of range!", lightSky);
LodUtil.assertTrue(lightBlock >= 0 && lightBlock < 16, "Trying to create datapoint with lightBlock[{}] out of range!", lightBlock);
LodUtil.assertTrue(alpha >= 0 && alpha < 256, "Trying to create datapoint with alpha[{}] out of range!", alpha);
LodUtil.assertTrue(red >= 0 && red < 256, "Trying to create datapoint with red[{}] out of range!", red);
LodUtil.assertTrue(green >= 0 && green < 256, "Trying to create datapoint with green[{}] out of range!", green);
LodUtil.assertTrue(blue >= 0 && blue < 256, "Trying to create datapoint with blue[{}] out of range!", blue);
LodUtil.assertTrue(generationMode >= 0 && generationMode < 8, "Trying to create datapoint with genMode[{}] out of range!", generationMode);
LodUtil.assertTrue(depth <= height, "Trying to create datapoint with depth[{}] greater than height[{}]!", depth, height);
return (long) (alpha >>> ALPHA_DOWNSIZE_SHIFT) << ALPHA_SHIFT
| (red & RED_MASK) << RED_SHIFT
| (green & GREEN_MASK) << GREEN_SHIFT
| (blue & BLUE_MASK) << BLUE_SHIFT
| (height & HEIGHT_MASK) << HEIGHT_SHIFT
| (depth & DEPTH_MASK) << DEPTH_SHIFT
| (lightBlock & BLOCK_LIGHT_MASK) << BLOCK_LIGHT_SHIFT
| (lightSky & SKY_LIGHT_MASK) << SKY_LIGHT_SHIFT
| (generationMode & GEN_TYPE_MASK) << GEN_TYPE_SHIFT
;
}
public static long shiftHeightAndDepth(long dataPoint, short offset) {
long height = (dataPoint + ((long) offset << HEIGHT_SHIFT)) & HEIGHT_SHIFTED_MASK;
long depth = (dataPoint + (offset << DEPTH_SHIFT)) & DEPTH_SHIFTED_MASK;
return dataPoint & ~(HEIGHT_SHIFTED_MASK | DEPTH_SHIFTED_MASK) | height | depth;
}
public static long version9Reorder(long dataPoint) {
/*
|a |a |a |a |r |r |r |r |
|r |r |r |r |g |g |g |g |
|g |g |g |g |b |b |b |b |
|b |b |b |b |h |h |h |h |
|h |h |h |h |h |h |d |d |
|d |d |d |d |d |d |d |d |
|bl |bl |bl |bl |sl |sl |sl |sl |
|l |l |f |g |g |g |v |e |
*/
if ((dataPoint & 1) == 0) return 0;
long height = (dataPoint >>> 26) & 0x3FF;
long depth = (dataPoint >>> 16) & 0x3FF;
if (height == depth || (dataPoint & 0b10) == 0b10) {
return createVoidDataPoint((byte) (((dataPoint >>> 2) & 0b111) + 1));
}
return ((dataPoint >>> 60) & 0xF) << ALPHA_SHIFT
| ((dataPoint >>> 52) & 0xFF) << RED_SHIFT
| ((dataPoint >>> 44) & 0xFF) << GREEN_SHIFT
| ((dataPoint >>> 36) & 0xFF) << BLUE_SHIFT
| ((dataPoint >>> 26) & 0x3FF) << HEIGHT_SHIFT
| ((dataPoint >>> 16) & 0x3FF) << DEPTH_SHIFT
| ((dataPoint >>> 8) & 0xFF) << SKY_LIGHT_SHIFT
| (((dataPoint >>> 2) & 0xFF) + 1) << GEN_TYPE_SHIFT;
}
public static short getHeight(long dataPoint) {
return (short) ((dataPoint >>> HEIGHT_SHIFT) & HEIGHT_MASK);
}
public static short getDepth(long dataPoint) {
return (short) ((dataPoint >>> DEPTH_SHIFT) & DEPTH_MASK);
}
public static short getAlpha(long dataPoint) {
return (short) ((((dataPoint >>> ALPHA_SHIFT) & ALPHA_MASK) << ALPHA_DOWNSIZE_SHIFT) | 0b1111);
}
public static short getRed(long dataPoint) {
return (short) ((dataPoint >>> RED_SHIFT) & RED_MASK);
}
public static short getGreen(long dataPoint) {
return (short) ((dataPoint >>> GREEN_SHIFT) & GREEN_MASK);
}
public static short getBlue(long dataPoint) {
return (short) ((dataPoint >>> BLUE_SHIFT) & BLUE_MASK);
}
public static byte getLightSky(long dataPoint) {
return (byte) ((dataPoint >>> SKY_LIGHT_SHIFT) & SKY_LIGHT_MASK);
}
public static byte getLightBlock(long dataPoint) {
return (byte) ((dataPoint >>> BLOCK_LIGHT_SHIFT) & BLOCK_LIGHT_MASK);
}
private static final SpamReducedLogger warnLogger = new SpamReducedLogger(1);
public static byte getGenerationMode(long dataPoint) {
byte genMode = (byte) ((dataPoint >>> GEN_TYPE_SHIFT) & GEN_TYPE_MASK);
if (warnLogger.canMaybeLog() && doesItExist(dataPoint) && genMode == 0) {
warnLogger.warnInc("Existing datapoint with genMode 0 detected! This is invalid in DataPoint version 10!"
+ " This may be caused by old data that has not been updated correctly.");
return 1;
}
return genMode == 0 ? 1 : genMode;
}
public static boolean isVoid(long dataPoint) {
return (((dataPoint >>> DEPTH_SHIFT) & HEIGHT_DEPTH_MASK) == 0);
}
public static boolean doesItExist(long dataPoint) {
return dataPoint != 0;
}
public static int getColor(long dataPoint) {
long alpha = getAlpha(dataPoint);
return (int) (((dataPoint >>> COLOR_SHIFT) & COLOR_MASK) | (alpha << (ALPHA_SHIFT - COLOR_SHIFT)));
}
/**
* This is used to convert a dataPoint to string (useful for the print function)
*/
@SuppressWarnings("unused")
public static String toString(long dataPoint) {
if (!doesItExist(dataPoint)) return "null";
if (isVoid(dataPoint)) return "void";
return "H:" + getHeight(dataPoint) +
" D:" + getDepth(dataPoint) +
" argb:" + getAlpha(dataPoint) + " " +
getRed(dataPoint) + " " +
getBlue(dataPoint) + " " +
getGreen(dataPoint) +
" BL/SL:" + getLightBlock(dataPoint) + " " +
getLightSky(dataPoint) +
" G:" + getGenerationMode(dataPoint);
}
private static void shrinkArray(short[] array, int packetSize, int start, int length, int arraySize) {
start *= packetSize;
length *= packetSize;
arraySize *= packetSize;
//remove comment to not leave garbage at the end
//array[start + packetSize + i] = 0;
if (arraySize - start >= 0) System.arraycopy(array, start + length, array, start, arraySize - start);
}
private static void extendArray(short[] array, int packetSize, int start, int length, int arraySize) {
start *= packetSize;
length *= packetSize;
arraySize *= packetSize;
for (int i = arraySize - start - 1; i >= 0; i--) {
array[start + length + i] = array[start + i];
array[start + i] = 0;
}
}
/**
* Return (>0) if dataA should replace dataB, (0) if equal, (<0) if dataB should replace dataA
*/
public static int compareDatapointPriority(long dataA, long dataB) {
return (int) ((dataA >> COMPARE_SHIFT) - (dataB >> COMPARE_SHIFT));
}
private static final ThreadLocal<int[]> tLocalIndeces = new ThreadLocal<int[]>();
private static final ThreadLocal<boolean[]> tLocalIncreaseIndex = new ThreadLocal<boolean[]>();
private static final ThreadLocal<boolean[]> tLocalIndexHandled = new ThreadLocal<boolean[]>();
private static final ThreadLocal<short[]> tLocalHeightAndDepth = new ThreadLocal<short[]>();
private static final ThreadLocal<int[]> tDataIndexCache = new ThreadLocal<int[]>();
/**
* This method merge column of multiple data together
*
* @param sourceData one or more columns of data
* @param output one column of space for the result to be written to
*/
public static void mergeMultiData(IColumnDataView sourceData, ColumnArrayView output) {
if (output.dataCount() != 1)
throw new IllegalArgumentException("output must be only reserved for one datapoint!");
int inputVerticalSize = sourceData.verticalSize();
int outputVerticalSize = output.verticalSize();
output.fill(0);
//dataCount indicate how many position we are merging in one position
int dataCount = sourceData.dataCount();
// We initialize the arrays that are going to be used
int heightAndDepthLength = (MAX_WORLD_Y_SIZE / 2 + 16) * 2;
short[] heightAndDepth = tLocalHeightAndDepth.get();
if (heightAndDepth == null || heightAndDepth.length != heightAndDepthLength) {
heightAndDepth = new short[heightAndDepthLength];
tLocalHeightAndDepth.set(heightAndDepth);
}
byte genMode = getGenerationMode(sourceData.get(0));
if (genMode == 0) genMode = 1; // FIXME: Hack to make the version 10 genMode never be 0.
boolean allEmpty = true;
boolean allVoid = true;
boolean limited = false;
boolean allDefault;
long singleData;
short depth;
short height;
int count = 0;
int i;
int ii;
int dataIndex;
int[] indeces = tLocalIndeces.get();
if (indeces == null || indeces.length != dataCount) {
indeces = new int[dataCount];
tLocalIndeces.set(indeces);
}
Arrays.fill(indeces, 0);
boolean[] increaseIndex = tLocalIncreaseIndex.get();
if (increaseIndex == null || increaseIndex.length != dataCount) {
increaseIndex = new boolean[dataCount];
tLocalIncreaseIndex.set(increaseIndex);
}
boolean[] indexHandled = tLocalIndexHandled.get();
if (indexHandled == null || indexHandled.length != dataCount) {
indexHandled = new boolean[dataCount];
tLocalIndexHandled.set(indexHandled);
}
long tempData;
for (int index = 0; index < dataCount; index++) {
tempData = sourceData.get(index * inputVerticalSize);
allVoid = allVoid && DataPointUtil.isVoid(tempData);
allEmpty = allEmpty && !DataPointUtil.doesItExist(tempData);
}
//We check if there is any data that's not empty or void
if (allEmpty) {
return;
}
if (allVoid) {
output.set(0, createVoidDataPoint(genMode));
return;
}
//this check is used only to see if we have checked all the values in the array
boolean stillHasDataToCheck = true;
short prevDepth;
while (stillHasDataToCheck) {
Arrays.fill(indexHandled, false);
boolean connected = true;
int newHeight = -10000;
int newDepth = -10000;
int tempHeight;
int tempDepth;
while (connected) {
Arrays.fill(increaseIndex, false);
for (int index = 0; index < dataCount; index++) {
if (indeces[index] < inputVerticalSize) {
tempData = sourceData.get(index * inputVerticalSize + indeces[index]);
if (!DataPointUtil.isVoid(tempData) && DataPointUtil.doesItExist(tempData)) {
tempHeight = DataPointUtil.getHeight(tempData);
tempDepth = DataPointUtil.getDepth(tempData);
if (tempDepth >= newHeight) {
//First case
//the column we are checking is higher than the current column
newDepth = tempDepth;
newHeight = tempHeight;
Arrays.fill(increaseIndex, false);
Arrays.fill(indexHandled, false);
increaseIndex[index] = true;
indexHandled[index] = true;
} else if ((tempDepth >= newDepth) && (tempHeight <= newHeight)) {
//the column we are checking is contained in the current column
//we simply increase this index
increaseIndex[index] = true;
indexHandled[index] = true;
} else if (tempHeight > newHeight && tempDepth <= newDepth) {
newDepth = tempDepth;
newHeight = tempHeight;
increaseIndex[index] = true;
indexHandled[index] = true;
} else if (tempHeight > newDepth && tempHeight <= newHeight) {
//the column we are checking touches the current column from the bottom
//for this reason we extend what's below
//We want to avoid to expend this column if it has already been expanded by
//this index
if (!indexHandled[index]) {
newDepth = tempDepth;
increaseIndex[index] = true;
indexHandled[index] = true;
}
} else if (tempDepth < newHeight && tempDepth > newDepth) {
//the column we are checking touches the current column from the top
//for this reason we extend the top
newHeight = tempHeight;
increaseIndex[index] = true;
}
} else {
indexHandled[index] = true;
}
}
}
//if we added any new data there is a chance that we could add more
//for this reason we would continue
//if no data is added than the column hasn't changed.
//for this reason we can start working on a new column
connected = false;
for (int index = 0; index < dataCount; index++) {
if (increaseIndex[index]) {
connected = true;
indeces[index]++;
}
}
}
//Now we add the height and depth data we extracted to the heightAndDepth array
if (newDepth != newHeight) {
if (count != 0) {
prevDepth = heightAndDepth[(count - 1) * 2 + 1];
if (newHeight > prevDepth) {
newHeight = (short) Math.min(newHeight, prevDepth);
}
}
heightAndDepth[count * 2] = (short) newHeight;
heightAndDepth[count * 2 + 1] = (short) newDepth;
count++;
}
//Here we check the condition that makes the loop continue
//We stop the loop only if there is no more data to check
stillHasDataToCheck = false;
for (int index = 0; index < dataCount; index++) {
if (indeces[index] < inputVerticalSize) {
tempData = sourceData.get(index * inputVerticalSize + indeces[index]);
stillHasDataToCheck |= !DataPointUtil.isVoid(tempData) && DataPointUtil.doesItExist(tempData);
}
}
}
//we limit the vertical portion to maxVerticalData
int j = 0;
while (count > outputVerticalSize) {
limited = true;
ii = MAX_WORLD_Y_SIZE;
for (i = 0; i < count - 1; i++) {
if (heightAndDepth[i * 2 + 1] - heightAndDepth[(i + 1) * 2] <= ii) {
ii = heightAndDepth[i * 2 + 1] - heightAndDepth[(i + 1) * 2];
j = i;
}
}
heightAndDepth[j * 2 + 1] = heightAndDepth[(j + 1) * 2 + 1];
for (i = j + 1; i < count - 1; i++) {
heightAndDepth[i * 2] = heightAndDepth[(i + 1) * 2];
heightAndDepth[i * 2 + 1] = heightAndDepth[(i + 1) * 2 + 1];
}
//System.arraycopy(heightAndDepth, j + 1, heightAndDepth, j, count - j - 1);
count--;
}
//As standard the vertical lods are ordered from top to bottom
if (!limited && dataCount == 1) // This mean source vertSize < output vertSize AND both dataCount == 1
{
sourceData.copyTo(output.data, output.offset, output.vertSize);
} else {
//We want to efficiently memorize indexes
int[] dataIndexesCache = tDataIndexCache.get();
if (dataIndexesCache == null || dataIndexesCache.length != dataCount) {
dataIndexesCache = new int[dataCount];
tDataIndexCache.set(dataIndexesCache);
}
Arrays.fill(dataIndexesCache, 0);
//For each lod height-depth value we have found we now want to generate the rest of the data
//by merging all lods at lower level that are contained inside the new ones
for (j = 0; j < count; j++) {
//We firstly collect height and depth data
//this will be added to each realtive long DataPoint
height = heightAndDepth[j * 2];
depth = heightAndDepth[j * 2 + 1];
//if both height and depth are at 0 then we finished
if ((depth == 0 && height == 0) || j >= heightAndDepth.length / 2)
break;
//We initialize data useful for the merge
int numberOfChildren = 0;
allEmpty = true;
allVoid = true;
//We initialize all the new values that we are going to put in the dataPoint
int tempAlpha = 0;
int tempRed = 0;
int tempGreen = 0;
int tempBlue = 0;
int tempLightBlock = 0;
int tempLightSky = 0;
long data = 0;
//For each position that we want to merge
for (int index = 0; index < dataCount; index++) {
//we scan the lods in the position from top to bottom
while (dataIndexesCache[index] < inputVerticalSize) {
singleData = sourceData.get(index * inputVerticalSize + dataIndexesCache[index]);
if (doesItExist(singleData) && !isVoid(singleData)) {
dataIndexesCache[index]++;
if ((depth <= getDepth(singleData) && getDepth(singleData) < height)
|| (depth < getHeight(singleData) && getHeight(singleData) <= height)) {
data = singleData;
break;
}
} else
break;
}
if (!doesItExist(data)) {
data = createVoidDataPoint(genMode);
}
if (doesItExist(data)) {
allEmpty = false;
if (!isVoid(data)) {
numberOfChildren++;
allVoid = false;
tempAlpha = Math.max(getAlpha(data), tempAlpha);
tempRed += getRed(data) * getRed(data);
tempGreen += getGreen(data) * getGreen(data);
tempBlue += getBlue(data) * getBlue(data);
tempLightBlock += getLightBlock(data);
tempLightSky += getLightSky(data);
}
}
}
//we have at least 1 child
if (dataCount != 1) {
tempRed = tempRed / numberOfChildren;
tempGreen = tempGreen / numberOfChildren;
tempBlue = tempBlue / numberOfChildren;
tempLightBlock = tempLightBlock / numberOfChildren;
tempLightSky = tempLightSky / numberOfChildren;
}
//data = createDataPoint(tempAlpha, tempRed, tempGreen, tempBlue, height, depth, tempLightSky, tempLightBlock, tempGenMode, allDefault);
//if (j > 0 && getColor(data) == getColor(dataPoint[j]))
//{
// add simplification at the end due to color
//}
output.set(j, createDataPoint(tempAlpha, (int) Math.sqrt(tempRed), (int) Math.sqrt(tempGreen), (int) Math.sqrt(tempBlue), height, depth, tempLightSky, tempLightBlock, genMode));
}
}
}
}
@@ -0,0 +1,130 @@
package com.seibel.lod.core.a7.datatype.column.accessor;
public class ColumnQuadView implements IColumnDataView {
private final long[] data;
private final int perColumnOffset; // per column (of columns of data) offset in longs
private final int xSize; // x size in datapoints
private final int zSize; // x size in datapoints
private final int offset; // offset in longs
private final int vertSize; // vertical size in longs
public ColumnQuadView(long[] data, int dataZWidth, int dataVertSize, int viewXOffset, int viewZOffset, int xSize, int zSize) {
if (viewXOffset + xSize > (data.length / (dataZWidth* dataVertSize)) || viewZOffset + zSize > dataZWidth)
throw new IllegalArgumentException("View is out of bounds");
this.data = data;
this.xSize = xSize;
this.zSize = zSize;
this.vertSize = dataVertSize;
this.perColumnOffset = dataZWidth * dataVertSize;
this.offset = viewXOffset * perColumnOffset + viewZOffset * dataVertSize;
}
private ColumnQuadView(long[] data, int perColumnOffset, int offset, int vertSize, int xSize, int zSize) {
this.data = data;
this.perColumnOffset = perColumnOffset;
this.offset = offset;
this.vertSize = vertSize;
this.xSize = xSize;
this.zSize = zSize;
}
@Override
public long get(int index) {
int x = index / perColumnOffset;
int z = (index % perColumnOffset) / vertSize;
int v = index % vertSize;
return get(x, z, v);
}
public long get(int x, int z, int v) {
return data[offset + x * perColumnOffset + z * vertSize + v];
}
public long set(int x, int z, int v, long value) {
return data[offset + x * perColumnOffset + z * vertSize + v] = value;
}
public ColumnArrayView get(int x, int z) {
return new ColumnArrayView(data, vertSize, offset + x * perColumnOffset + z * vertSize, vertSize);
}
public ColumnArrayView getRow(int x) {
return new ColumnArrayView(data, zSize * vertSize, offset + x * perColumnOffset, vertSize);
}
public void set(int x, int z, IColumnDataView singleColumn) {
if (singleColumn.verticalSize() != vertSize) throw new IllegalArgumentException("Vertical size of singleColumn must be equal to vertSize");
if (singleColumn.dataCount() != 1) throw new IllegalArgumentException("SingleColumn must contain exactly one data point");
singleColumn.copyTo(data, offset + x * perColumnOffset + z * vertSize, singleColumn.size());
}
@Override
public int size() {
return xSize * zSize * vertSize;
}
@Override
public int verticalSize() {
return vertSize;
}
@Override
public int dataCount() {
return xSize * zSize;
}
@Override
public IColumnDataView subView(int dataIndexStart, int dataCount) {
if (dataCount != 1) throw new UnsupportedOperationException("Fixme: subView for QUadView only support one data point!");
int x = dataIndexStart / xSize;
int z = dataIndexStart % xSize;
return new ColumnArrayView(data, vertSize * dataCount, offset + x * perColumnOffset + z * vertSize, vertSize);
}
public ColumnQuadView subView(int xOffset, int zOffset, int xSize, int zSize) {
if (xOffset + xSize > this.xSize || zOffset + zSize > this.zSize) throw new IllegalArgumentException("SubView is out of bounds");
return new ColumnQuadView(data, perColumnOffset, offset + xOffset * perColumnOffset + zOffset * vertSize, vertSize, xSize, zSize);
}
@Override
public void copyTo(long[] target, int offset, int size) {
if (size != this.size() && size > zSize * vertSize) throw new UnsupportedOperationException("Not supported yet");
if (size <= xSize * vertSize) {
System.arraycopy(data, this.offset, target, offset, size);
} else {
for (int x = 0; x < xSize; x++) {
System.arraycopy(data, this.offset + x * perColumnOffset, target, offset + x * xSize * vertSize, xSize * vertSize);
}
}
}
public void copyTo(ColumnQuadView target) {
if (target.xSize != xSize || target.zSize != zSize)
throw new IllegalArgumentException("Target view must have same size as this view");
for (int x = 0; x < xSize; x++) {
target.getRow(x).changeVerticalSizeFrom(getRow(x));
}
}
public void mergeMultiColumnFrom(ColumnQuadView source) {
if (source.xSize == xSize && source.zSize == zSize)
{
source.copyTo(this);
return;
}
if (source.xSize < xSize || source.zSize < zSize)
throw new IllegalArgumentException("Source view must have same or larger size as this view");
int srcXPerTrgX = source.xSize / xSize;
int srcZPerTrgZ = source.zSize / zSize;
if (source.xSize % xSize != 0 || source.zSize % zSize != 0)
throw new IllegalArgumentException("Source view's size must be a multiple of this view's size");
for (int x = 0; x < xSize; x++) {
for (int z = 0; z < zSize; z++) {
ColumnQuadView srcBlock = source.subView(x * srcXPerTrgX, z * srcZPerTrgZ, srcXPerTrgX, srcZPerTrgZ);
get(x, z).mergeMultiDataFrom(srcBlock);
}
}
}
}
@@ -0,0 +1,33 @@
package com.seibel.lod.core.a7.datatype.column.accessor;
import java.util.Iterator;
public interface IColumnDataView {
long get(int index);
int size();
default Iterator<Long> iterator() {
return new Iterator<Long>() {
private int index = 0;
private final int size = size();
@Override
public boolean hasNext() {
return index < size;
}
@Override
public Long next() {
return get(index++);
}
};
}
int verticalSize();
int dataCount();
IColumnDataView subView(int dataIndexStart, int dataCount);
void copyTo(long[] target, int offset, int count);
}
@@ -0,0 +1,36 @@
package com.seibel.lod.core.a7.datatype.column.accessor;
import com.seibel.lod.core.objects.LodDataView;
public interface IColumnDatatype {
byte getDetailOffset();
default int getDataSize() {
return 1 << getDetailOffset();
}
int getMaxNumberOfLods();
long getRoughRamUsage();
int getVerticalSize();
boolean doesItExist(int posX, int posZ);
long getData(int posX, int posZ, int verticalIndex);
default long getSingleData(int posX, int posZ) {return getData(posX, posZ, 0);}
long[] getAllData(int posX, int posZ);
ColumnArrayView getVerticalDataView(int posX, int posZ);
ColumnQuadView getDataInQuad(int quadX, int quadZ, int quadXSize, int quadZSize);
ColumnQuadView getFullQuad();
/**
* This method will clear all data at relative section position
*/
void clear(int posX, int posZ);
/**
* This method will add the data given in input at the relative position and vertical index
*/
boolean addData(long data, int posX, int posZ, int verticalIndex);
/**
* This methods will add the data in the given position if certain condition are satisfied
* @param override if override is true we can override data created with same generation mode
*/
boolean copyVerticalData(LodDataView data, int posX, int posZ, boolean override);
void generateData(IColumnDatatype lowerDataContainer, int posX, int posZ);
}
@@ -0,0 +1,348 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.a7.datatype.column.render;
import com.seibel.lod.core.a7.datatype.column.accessor.ColumnArrayView;
import com.seibel.lod.core.a7.render.a7LodRenderer;
import com.seibel.lod.core.builders.lodBuilding.bufferBuilding.LodQuadBuilder;
import com.seibel.lod.core.config.Config;
import com.seibel.lod.core.enums.ELodDirection;
import com.seibel.lod.core.handlers.dependencyInjection.SingletonInjector;
import com.seibel.lod.core.util.ColorUtil;
import com.seibel.lod.core.util.DataPointUtil;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
public class ColumnBox
{
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
public static void addBoxQuadsToBuilder(LodQuadBuilder builder, short xSize, short ySize, short zSize, short x,
short y, short z, int color, byte skyLight, byte blockLight, long topData, long botData, ColumnArrayView[][] adjData)
{
short maxX = (short) (x + xSize);
short maxY = (short) (y + ySize);
short maxZ = (short) (z + zSize);
byte skyLightTop = skyLight;
byte skyLightBot = DataPointUtil.doesItExist(botData) ? DataPointUtil.getLightSky(botData) : 0;
boolean isTransparent = ColorUtil.getAlpha(color)<255 && a7LodRenderer.transparencyEnabled;
boolean isTopTransparent = DataPointUtil.getAlpha(topData)<255 && a7LodRenderer.transparencyEnabled;
boolean isBotTransparent = DataPointUtil.getAlpha(botData)<255 && a7LodRenderer.transparencyEnabled;
// Up direction case
//We skip if
// current block is not transparent: we check if the adj block is attached and opaque
boolean skipTop = DataPointUtil.doesItExist(topData) && (DataPointUtil.getDepth(topData) == maxY) && !isTopTransparent;
boolean skipBot = DataPointUtil.doesItExist(botData) && (DataPointUtil.getHeight(botData) == y) && !isBotTransparent;
if(a7LodRenderer.transparencyEnabled && a7LodRenderer.fakeOceanFloor) {
if (!isTransparent && isTopTransparent && DataPointUtil.doesItExist(topData)) {
skyLightTop = (byte) LodUtil.clamp(0, 15 - (DataPointUtil.getHeight(topData) - y), 15);
ySize = (short) (DataPointUtil.getHeight(topData) - y - 1);
//y = (short) (DataPointUtil.getHeight(topData) - 2);
//ySize = 1;
} else if (isTransparent && !isBotTransparent && DataPointUtil.doesItExist(botData)) {
y = (short) (y + ySize - 1);
ySize = 1;
}
maxY = (short) (y + ySize);
}
if (!skipTop)
builder.addQuadUp(x, maxY, z, xSize, zSize, ColorUtil.applyShade(color, MC.getShade(ELodDirection.UP)), skyLightTop, blockLight);
if (!skipBot)
builder.addQuadDown(x, y, z, xSize, zSize, ColorUtil.applyShade(color, MC.getShade(ELodDirection.DOWN)), skyLightBot, blockLight);
if(isTransparent)
return;
//If the adj pos is at the same level we cull the faces normally, otherwise we divide the face in two and cull the two part separately
//NORTH face vertex creation
{
ColumnArrayView[] adjDataNorth = adjData[ELodDirection.NORTH.ordinal() - 2];
int adjOverlapNorth = ColorUtil.TRANSPARENT;
if (adjDataNorth == null)
{
builder.addQuadAdj(ELodDirection.NORTH, x, y, z, xSize, ySize, color, (byte) 15, blockLight);
}
else if (adjDataNorth.length == 1)
{
makeAdjQuads(builder, adjDataNorth[0], ELodDirection.NORTH, x, y, z, xSize, ySize,
color, adjOverlapNorth, skyLightTop, blockLight);
}
else
{
makeAdjQuads(builder, adjDataNorth[0], ELodDirection.NORTH, x, y, z, (short) (xSize / 2), ySize,
color, adjOverlapNorth, skyLightTop, blockLight);
makeAdjQuads(builder, adjDataNorth[1], ELodDirection.NORTH, (short) (x + xSize / 2), y, z, (short) (xSize / 2), ySize,
color, adjOverlapNorth, skyLightTop, blockLight);
}
}
//SOUTH face vertex creation
{
ColumnArrayView[] adjDataSouth = adjData[ELodDirection.SOUTH.ordinal() - 2];
int adjOverlapSouth = ColorUtil.TRANSPARENT;
if (adjDataSouth == null)
{
builder.addQuadAdj(ELodDirection.SOUTH, x, y, maxZ, xSize, ySize, color, (byte) 15, blockLight);
}
else if (adjDataSouth.length == 1)
{
makeAdjQuads(builder, adjDataSouth[0], ELodDirection.SOUTH, x, y, maxZ, xSize, ySize,
color, adjOverlapSouth, skyLightTop, blockLight);
}
else
{
makeAdjQuads(builder, adjDataSouth[0], ELodDirection.SOUTH, x, y, maxZ, (short) (xSize / 2), ySize,
color, adjOverlapSouth, skyLightTop, blockLight);
makeAdjQuads(builder, adjDataSouth[1], ELodDirection.SOUTH, (short) (x + xSize / 2), y, maxZ, (short) (xSize / 2), ySize,
color, adjOverlapSouth, skyLightTop, blockLight);
}
}
//WEST face vertex creation
{
ColumnArrayView[] adjDataWest = adjData[ELodDirection.WEST.ordinal() - 2];
int adjOverlapWest = ColorUtil.TRANSPARENT;
if (adjDataWest == null)
{
builder.addQuadAdj(ELodDirection.WEST, x, y, z, zSize, ySize, color, (byte) 15, blockLight);
}
else if (adjDataWest.length == 1)
{
makeAdjQuads(builder, adjDataWest[0], ELodDirection.WEST, x, y, z, zSize, ySize,
color, adjOverlapWest, skyLightTop, blockLight);
}
else
{
makeAdjQuads(builder, adjDataWest[0], ELodDirection.WEST, x, y, z, (short) (zSize / 2), ySize,
color, adjOverlapWest, skyLightTop, blockLight);
makeAdjQuads(builder, adjDataWest[1], ELodDirection.WEST, x, y, (short) (z + zSize / 2), (short) (zSize / 2), ySize,
color, adjOverlapWest, skyLightTop, blockLight);
}
}
//EAST face vertex creation
{
ColumnArrayView[] adjDataEast = adjData[ELodDirection.EAST.ordinal() - 2];
int adjOverlapEast = ColorUtil.TRANSPARENT;
if (adjData[ELodDirection.EAST.ordinal() - 2] == null)
{
builder.addQuadAdj(ELodDirection.EAST, maxX, y, z, zSize, ySize, color, (byte) 15, blockLight);
}
else if (adjDataEast.length == 1)
{
makeAdjQuads(builder, adjDataEast[0], ELodDirection.EAST, maxX, y, z, zSize, ySize,
color, adjOverlapEast, skyLightTop, blockLight);
}
else
{
makeAdjQuads(builder, adjDataEast[0], ELodDirection.EAST, maxX, y, z, (short) (zSize / 2), ySize,
color, adjOverlapEast, skyLightTop, blockLight);
makeAdjQuads(builder, adjDataEast[1], ELodDirection.EAST, maxX, y, (short) (z + zSize / 2), (short) (zSize / 2), ySize,
color, adjOverlapEast, skyLightTop, blockLight);
}
}
}
private static void makeAdjQuads(LodQuadBuilder builder, ColumnArrayView adjData, ELodDirection direction, short x, short y,
short z, short w0, short wy, int color, int overlapColor, byte upSkyLight, byte blockLight)
{
color = ColorUtil.applyShade(color, MC.getShade(direction));
ColumnArrayView dataPoint = adjData;
if (dataPoint == null || DataPointUtil.isVoid(dataPoint.get(0)))
{
builder.addQuadAdj(direction, x, y, z, w0, wy, color, (byte) 15, blockLight);
return;
}
int i;
boolean firstFace = true;
boolean allAbove = true;
short previousDepth = -1;
byte nextSkyLight = upSkyLight;
boolean isTransparent = ColorUtil.getAlpha(color) < 255 && a7LodRenderer.transparencyEnabled;
boolean lastWasTransparent = false;
for (i = 0; i < dataPoint.size() && DataPointUtil.doesItExist(adjData.get(i))
&& !DataPointUtil.isVoid(adjData.get(i)); i++)
{
long adjPoint = adjData.get(i);
boolean isAdjTransparent = DataPointUtil.getAlpha(adjPoint) < 255 && a7LodRenderer.transparencyEnabled;
if (!isTransparent && isAdjTransparent && a7LodRenderer.transparencyEnabled)
continue;
short height = DataPointUtil.getHeight(adjPoint);
short depth = DataPointUtil.getDepth(adjPoint);
if(a7LodRenderer.transparencyEnabled && a7LodRenderer.fakeOceanFloor)
{
if(lastWasTransparent && !isAdjTransparent)
{
height = (short) (DataPointUtil.getHeight(adjData.get(i-1)) - 1);
}
else if(isAdjTransparent && (i + 1) < adjData.size())
{
if (DataPointUtil.getAlpha(adjData.get(i+1)) == 255)
{
depth = (short) (height - 1);
}
}
}
// If the depth of said block is higher than our max Y, continue
// Basically: y < maxY <= _____ height
// _______&&: y < maxY <= depth
if (y + wy <= depth)
continue;
// Now: depth < maxY
allAbove = false;
if (height < y)
{
// Basically: _____ height < y < maxY
// _______&&: depth ______ < y < maxY
if (firstFace)
{
builder.addQuadAdj(direction, x, y, z, w0, wy, color, DataPointUtil.getLightSky(adjPoint),
blockLight);
}
else
{
// Now: depth < height < y < previousDepth < maxY
if (previousDepth == -1)
throw new RuntimeException("Loop error");
builder.addQuadAdj(direction, x, y, z, w0, (short) (previousDepth - y), color,
DataPointUtil.getLightSky(adjPoint), blockLight);
previousDepth = -1;
}
break;
}
if (depth <= y)
{ // AND y <= height
if (y + wy <= height)
{
// Basically: ________ y < maxY <= height
// _______&&: depth <= y < maxY
// The face is inside adj face completely.
if (overlapColor != 0)
{
builder.addQuadAdj(direction, x, y, z, w0, wy, overlapColor, (byte) 15, (byte) 15);
}
break;
}
// Otherwise: ________ y <= Height < maxY
// _______&&: depth <= y _________ < maxY
// the adj data intersects the lower part of the current data
if (height > y && overlapColor != 0)
{
builder.addQuadAdj(direction, x, y, z, w0, (short) (height - y), overlapColor, (byte) 15, (byte) 15);
}
// if this is the only face, use the maxY and break,
// if there was another face we finish the last one and break
if (firstFace)
{
builder.addQuadAdj(direction, x, height, z, w0, (short) (y + wy - height), color,
DataPointUtil.getLightSky(adjPoint), blockLight);
}
else
{
// Now: depth <= y <= height <= previousDepth < maxY
if (previousDepth == -1)
throw new RuntimeException("Loop error");
if (previousDepth > height)
{
builder.addQuadAdj(direction, x, height, z, w0, (short) (previousDepth - height), color,
DataPointUtil.getLightSky(adjPoint), blockLight);
}
previousDepth = -1;
}
break;
}
// In here always true: y < depth < maxY
// _________________&&: y < _____ (height and maxY)
if (y + wy <= height)
{
// Basically: y _______ < maxY <= height
// _______&&: y < depth < maxY
// the adj data intersects the higher part of the current data
if (overlapColor != 0)
{
builder.addQuadAdj(direction, x, depth, z, w0, (short) (y + wy - depth), overlapColor, (byte) 15, (byte) 15);
}
// we start the creation of a new face
}
else
{
// Otherwise: y < _____ height < maxY
// _______&&: y < depth ______ < maxY
if (overlapColor != 0)
{
builder.addQuadAdj(direction, x, depth, z, w0, (short) (height - depth), overlapColor, (byte) 15, (byte) 15);
}
if (firstFace)
{
builder.addQuadAdj(direction, x, height, z, w0, (short) (y + wy - height), color,
DataPointUtil.getLightSky(adjPoint), blockLight);
}
else
{
// Now: y < depth < height <= previousDepth < maxY
if (previousDepth == -1)
throw new RuntimeException("Loop error");
if (previousDepth > height)
{
builder.addQuadAdj(direction, x, height, z, w0, (short) (previousDepth - height), color,
DataPointUtil.getLightSky(adjPoint), blockLight);
}
previousDepth = -1;
}
}
// set next top as current depth
previousDepth = depth;
firstFace = false;
nextSkyLight = upSkyLight;
if (i + 1 < adjData.size() && DataPointUtil.doesItExist(adjData.get(i + 1)))
nextSkyLight = DataPointUtil.getLightSky(adjData.get(i + 1));
lastWasTransparent = isAdjTransparent;
}
if (allAbove)
{
builder.addQuadAdj(direction, x, y, z, w0, wy, color, upSkyLight, blockLight);
}
else if (previousDepth != -1)
{
// We need to finish the last quad.
builder.addQuadAdj(direction, x, y, z, w0, (short) (previousDepth - y), color, nextSkyLight,
blockLight);
}
}
}
@@ -0,0 +1,392 @@
package com.seibel.lod.core.a7.datatype.column.render;
import com.seibel.lod.core.a7.datatype.column.ColumnRenderSource;
import com.seibel.lod.core.a7.datatype.column.accessor.ColumnArrayView;
import com.seibel.lod.core.a7.level.IClientLevel;
import com.seibel.lod.core.a7.render.a7LodRenderer;
import com.seibel.lod.core.a7.util.UncheckedInterruptedException;
import com.seibel.lod.core.a7.render.RenderBuffer;
import com.seibel.lod.core.config.Config;
import com.seibel.lod.core.builders.lodBuilding.bufferBuilding.CubicLodTemplate;
import com.seibel.lod.core.builders.lodBuilding.bufferBuilding.LodQuadBuilder;
import com.seibel.lod.core.enums.ELodDirection;
import com.seibel.lod.core.enums.config.EGpuUploadMethod;
import com.seibel.lod.core.enums.rendering.EDebugMode;
import com.seibel.lod.core.enums.rendering.EGLProxyContext;
import com.seibel.lod.core.logging.ConfigBasedLogger;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.objects.DHBlockPos;
import com.seibel.lod.core.render.GLProxy;
import com.seibel.lod.core.render.objects.GLVertexBuffer;
import com.seibel.lod.core.util.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.lang.invoke.MethodHandles;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.concurrent.*;
import static com.seibel.lod.core.render.GLProxy.GL_LOGGER;
public class ColumnRenderBuffer extends RenderBuffer {
//TODO: Make the pool use configurable number of threads
public static final ExecutorService BUFFER_BUILDERS = LodUtil.makeThreadPool(4, "BufferBuilder");
public static final ExecutorService BUFFER_UPLOADER = LodUtil.makeSingleThreadPool("ColumnBufferUploader");
public static final int MAX_CONCURRENT_CALL = 8;
public static final ConfigBasedLogger EVENT_LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Debugging.DebugSwitch.logRendererBufferEvent.get());
private static final Logger LOGGER = DhLoggerBuilder.getLogger(MethodHandles.lookup().lookupClass().getSimpleName());
private static final long MAX_BUFFER_UPLOAD_TIMEOUT_NANOSECONDS = 1_000_000;
GLVertexBuffer[] vbos;
public final DHBlockPos pos;
public ColumnRenderBuffer(DHBlockPos pos) {
this.pos = pos;
vbos = new GLVertexBuffer[0];
}
private void _uploadBuffersDirect(LodQuadBuilder builder, EGpuUploadMethod method) throws InterruptedException {
resize(builder.getCurrentNeededVertexBufferCount());
long remainingNS = 0;
long BPerNS = Config.Client.Advanced.Buffers.gpuUploadPerMegabyteInMilliseconds.get();
int i = 0;
Iterator<ByteBuffer> iter = builder.makeVertexBuffers();
while (iter.hasNext()) {
if (i >= vbos.length) {
throw new RuntimeException("Too many vertex buffers!!");
}
ByteBuffer bb = iter.next();
GLVertexBuffer vbo = getOrMakeVbo(i++, method.useBufferStorage);
int size = bb.limit() - bb.position();
try {
vbo.bind();
vbo.uploadBuffer(bb, size/LodUtil.LOD_VERTEX_FORMAT.getByteSize(), method, FULL_SIZED_BUFFER);
} catch (Exception e) {
vbos[i-1] = null;
vbo.close();
LOGGER.error("Failed to upload buffer: ", e);
}
if (BPerNS<=0) continue;
// upload buffers over an extended period of time
// to hopefully prevent stuttering.
remainingNS += size * BPerNS;
if (remainingNS >= TimeUnit.NANOSECONDS.convert(1000 / 60, TimeUnit.MILLISECONDS)) {
if (remainingNS > MAX_BUFFER_UPLOAD_TIMEOUT_NANOSECONDS)
remainingNS = MAX_BUFFER_UPLOAD_TIMEOUT_NANOSECONDS;
Thread.sleep(remainingNS / 1000000, (int) (remainingNS % 1000000));
remainingNS = 0;
}
}
if (i < vbos.length) {
throw new RuntimeException("Too few vertex buffers!!");
}
}
private void _uploadBuffersMapped(LodQuadBuilder builder, EGpuUploadMethod method)
{
resize(builder.getCurrentNeededVertexBufferCount());
for (int i=0; i<vbos.length; i++) {
if (vbos[i]==null) vbos[i] = new GLVertexBuffer(method.useBufferStorage);
}
LodQuadBuilder.BufferFiller func = builder.makeBufferFiller(method);
int i = 0;
while (i < vbos.length && func.fill(vbos[i++])) {}
}
private GLVertexBuffer getOrMakeVbo(int iIndex, boolean useBuffStorage) {
if (vbos[iIndex] == null) {
vbos[iIndex] = new GLVertexBuffer(useBuffStorage);
}
return vbos[iIndex];
}
private void resize(int size) {
if (vbos.length != size) {
GLVertexBuffer[] newVbos = new GLVertexBuffer[size];
if (vbos.length > size) {
for (int i=size; i<vbos.length; i++) {
if (vbos[i]!=null) vbos[i].close();
vbos[i] = null;
}
}
for (int i=0; i<newVbos.length && i<vbos.length; i++) {
newVbos[i] = vbos[i];
vbos[i] = null;
}
for (GLVertexBuffer b : vbos) {
if (b != null) throw new RuntimeException("LEAKING VBO!");
}
vbos = newVbos;
}
}
public void uploadBuffer(LodQuadBuilder builder, EGpuUploadMethod method) throws InterruptedException {
if (method.useEarlyMapping) {
_uploadBuffersMapped(builder, method);
} else {
_uploadBuffersDirect(builder, method);
}
}
@Override
public boolean render(a7LodRenderer renderContext) {
boolean hasRendered = false;
renderContext.setupOffset(pos);
for (GLVertexBuffer vbo : vbos) {
if (vbo == null) continue;
if (vbo.getVertexCount() == 0) continue;
hasRendered = true;
renderContext.drawVbo(vbo);
//LodRenderer.tickLogger.info("Vertex buffer: {}", vbo);
}
return hasRendered;
}
@Override
public void debugDumpStats(StatsMap statsMap) {
statsMap.incStat("RenderBuffers");
statsMap.incStat("SimpleRenderBuffers");
for (GLVertexBuffer b : vbos) {
if (b == null) continue;
statsMap.incStat("VBOs");
if (b.getSize() == FULL_SIZED_BUFFER) {
statsMap.incStat("FullsizedVBOs");
}
if (b.getSize() == 0) GL_LOGGER.warn("VBO with size 0");
statsMap.incBytesStat("TotalUsage", b.getSize());
}
}
private boolean closed = false;
@Override
public void close() {
if (closed) return;
closed = true;
GLProxy.getInstance().recordOpenGlCall(() -> {
for (GLVertexBuffer b : vbos) {
if (b == null) continue;
b.destroy(false);
}
});
}
private static long getCurrentJobsCount() {
long jobs = ((ThreadPoolExecutor) BUFFER_BUILDERS).getQueue().stream().filter(t -> !((Future<?>) t).isDone()).count();
jobs += ((ThreadPoolExecutor) BUFFER_UPLOADER).getQueue().stream().filter(t -> !((Future<?>) t).isDone()).count();
return jobs;
}
public static boolean isBusy() {
return getCurrentJobsCount() > MAX_CONCURRENT_CALL;
}
public static CompletableFuture<ColumnRenderBuffer[]> build(IClientLevel clientLevel, Reference<ColumnRenderBuffer> usedBufferSlotOpaque, Reference<ColumnRenderBuffer> usedBufferSlotTransparent, ColumnRenderSource data, ColumnRenderSource[] adjData) {
if (isBusy()) return null;
//LOGGER.info("RenderRegion startBuild @ {}", data.sectionPos);
return CompletableFuture.supplyAsync(() -> {
try {
EVENT_LOGGER.trace("RenderRegion start QuadBuild @ {}", data.sectionPos);
int skyLightCullingBelow = Config.Client.Graphics.AdvancedGraphics.caveCullingHeight.get();
// FIXME: Clamp also to the max world height.
skyLightCullingBelow = Math.max(skyLightCullingBelow, clientLevel.getMinY());
LodQuadBuilder builderOpaque = new LodQuadBuilder(true,
(short) (skyLightCullingBelow - clientLevel.getMinY()));
LodQuadBuilder builderTransparent = new LodQuadBuilder(true,
(short) (skyLightCullingBelow - clientLevel.getMinY()));
makeLodRenderData(builderOpaque, builderTransparent, data, adjData);
if (builderOpaque.getCurrentQuadsCount() > 0) {
//LOGGER.info("her");
}
EVENT_LOGGER.trace("RenderRegion end QuadBuild @ {}", data.sectionPos);
LodQuadBuilder[] builders = new LodQuadBuilder[2];
builders[0] = builderOpaque;
builders[1] = builderTransparent;
return builders;
} catch (UncheckedInterruptedException e) {
throw e;
}
catch (Throwable e3) {
LOGGER.error("\"LodNodeBufferBuilder\" was unable to build quads: ", e3);
throw e3;
}
}, BUFFER_BUILDERS)
.thenApplyAsync((builders) -> {
try {
EVENT_LOGGER.trace("RenderRegion start Upload @ {}", data.sectionPos);
GLProxy glProxy = GLProxy.getInstance();
EGpuUploadMethod method = GLProxy.getInstance().getGpuUploadMethod();
EGLProxyContext oldContext = glProxy.getGlContext();
glProxy.setGlContext(EGLProxyContext.LOD_BUILDER);
ColumnRenderBuffer buffersSlotOpaque = usedBufferSlotOpaque.swap(null);
ColumnRenderBuffer buffersSlotTransparent = usedBufferSlotTransparent.swap(null);
if (buffersSlotOpaque == null)
buffersSlotOpaque = new ColumnRenderBuffer(
new DHBlockPos(data.sectionPos.getCorner().getCorner(), clientLevel.getMinY())
);
if (buffersSlotTransparent == null)
buffersSlotTransparent = new ColumnRenderBuffer(
new DHBlockPos(data.sectionPos.getCorner().getCorner(), clientLevel.getMinY())
);
try {
buffersSlotOpaque.uploadBuffer(builders[0], method);
buffersSlotTransparent.uploadBuffer(builders[1], method);
EVENT_LOGGER.trace("RenderRegion end Upload @ {}", data.sectionPos);
ColumnRenderBuffer[] buffers = new ColumnRenderBuffer[2];
buffers[0] = buffersSlotOpaque;
buffers[1] = buffersSlotTransparent;
return buffers;
} catch (Exception e) {
buffersSlotOpaque.close();
buffersSlotTransparent.close();
throw e;
} finally {
glProxy.setGlContext(oldContext);
}
} catch (InterruptedException e) {
throw UncheckedInterruptedException.convert(e);
} catch (Throwable e3) {
LOGGER.error("\"LodNodeBufferBuilder\" was unable to upload buffer: ", e3);
throw e3;
}
}, BUFFER_UPLOADER).handle((v, e) -> {
//LOGGER.info("RenderRegion endBuild @ {}", data.sectionPos);
if (e != null) {
ColumnRenderBuffer buffersSlot;
if (!usedBufferSlotOpaque.isEmpty()) {
buffersSlot = usedBufferSlotOpaque.swap(null);
buffersSlot.close();
}
if (!usedBufferSlotTransparent.isEmpty()) {
buffersSlot = usedBufferSlotTransparent.swap(null);
buffersSlot.close();
}
return null;
} else {
return v;
}
});
}
private static void makeLodRenderData(LodQuadBuilder quadBuilderOpaque, LodQuadBuilder quadBuilderTransparent, ColumnRenderSource region, ColumnRenderSource[] adjRegions) {
// Variable initialization
EDebugMode debugMode = Config.Client.Advanced.Debugging.debugMode.get();
byte detailLevel = region.getDataDetail();
for (int x = 0; x < ColumnRenderSource.SECTION_SIZE; x++) {
for (int z = 0; z < ColumnRenderSource.SECTION_SIZE; z++) {
UncheckedInterruptedException.throwIfInterrupted();
ColumnArrayView posData = region.getVerticalDataView(x, z);
if (posData.size() == 0 || !DataPointUtil.doesItExist(posData.get(0))
|| DataPointUtil.isVoid(posData.get(0)))
continue;
ColumnArrayView[][] adjData = new ColumnArrayView[4][];
// We extract the adj data in the four cardinal direction
// we first reset the adjShadeDisabled. This is used to disable the shade on the
// border when we have transparent block like water or glass
// to avoid having a "darker border" underground
// Arrays.fill(adjShadeDisabled, false);
// We check every adj block in each direction
// If the adj block is rendered in the same region and with same detail
// and is positioned in a place that is not going to be rendered by vanilla game
// then we can set this position as adj
// We avoid cases where the adjPosition is in player chunk while the position is
// not
// to always have a wall underwater
for (ELodDirection lodDirection : ELodDirection.ADJ_DIRECTIONS) {
try {
int xAdj = x + lodDirection.getNormal().x;
int zAdj = z + lodDirection.getNormal().z;
boolean isCrossRegionBoundary = (xAdj < 0 || xAdj >= ColumnRenderSource.SECTION_SIZE) ||
(zAdj < 0 || zAdj >= ColumnRenderSource.SECTION_SIZE);
ColumnRenderSource adjRegion;
byte adjDetail;
//we check if the detail of the adjPos is equal to the correct one (region border fix)
//or if the detail is wrong by 1 value (region+circle border fix)
if (isCrossRegionBoundary) {
//we compute at which detail that position should be rendered
adjRegion = adjRegions[lodDirection.ordinal()-2];
if(adjRegion == null) continue;
adjDetail = adjRegion.getDataDetail();
if (adjDetail != detailLevel) {
//TODO: Implement this
} else {
if (xAdj < 0) xAdj += ColumnRenderSource.SECTION_SIZE;
if (zAdj < 0) zAdj += ColumnRenderSource.SECTION_SIZE;
if (xAdj >= ColumnRenderSource.SECTION_SIZE) xAdj -= ColumnRenderSource.SECTION_SIZE;
if (zAdj >= ColumnRenderSource.SECTION_SIZE) zAdj -= ColumnRenderSource.SECTION_SIZE;
}
} else {
adjRegion = region;
adjDetail = detailLevel;
}
if (adjDetail < detailLevel-1 || adjDetail > detailLevel+1) {
continue;
}
if (adjDetail == detailLevel || adjDetail > detailLevel) {
adjData[lodDirection.ordinal() - 2] = new ColumnArrayView[1];
adjData[lodDirection.ordinal() - 2][0] = adjRegion.getVerticalDataView(xAdj, zAdj);
} else {
adjData[lodDirection.ordinal() - 2] = new ColumnArrayView[2];
adjData[lodDirection.ordinal() - 2][0] = adjRegion.getVerticalDataView(xAdj, zAdj);
adjData[lodDirection.ordinal() - 2][1] = adjRegion.getVerticalDataView(
xAdj + (lodDirection.getAxis()== ELodDirection.Axis.X ? 0 : 1),
zAdj + (lodDirection.getAxis()== ELodDirection.Axis.Z ? 0 : 1));
}
} catch (RuntimeException e) {
EVENT_LOGGER.warn("Failed to get adj data for [{}:{},{}] at [{}]", detailLevel, x, z, lodDirection);
EVENT_LOGGER.warn("Detail exception: ", e);
}
}
// We render every vertical lod present in this position
// We only stop when we find a block that is void or non-existing block
for (int i = 0; i < posData.size(); i++) {
long data = posData.get(i);
// If the data is not renderable (Void or non-existing) we stop since there is
// no data left in this position
if (DataPointUtil.isVoid(data) || !DataPointUtil.doesItExist(data))
break;
long adjDataTop = i - 1 >= 0 ? posData.get(i - 1) : DataPointUtil.EMPTY_DATA;
long adjDataBot = i + 1 < posData.size() ? posData.get(i + 1) : DataPointUtil.EMPTY_DATA;
// We send the call to create the vertices
if(DataPointUtil.getAlpha(data) == 255 || !a7LodRenderer.transparencyEnabled)
{
CubicLodTemplate.addLodToBuffer(data, adjDataTop, adjDataBot, adjData, detailLevel,
x, z, quadBuilderOpaque, debugMode);
}
else
{
CubicLodTemplate.addLodToBuffer(data, adjDataTop, adjDataBot, adjData, detailLevel,
x, z, quadBuilderTransparent, debugMode);
}
}
}
}
quadBuilderOpaque.mergeQuads();
if(a7LodRenderer.transparencyEnabled)
quadBuilderTransparent.mergeQuads();
}
}
@@ -0,0 +1,36 @@
package com.seibel.lod.core.a7.datatype.full;
import com.seibel.lod.core.a7.datatype.full.accessor.FullArrayView;
import com.seibel.lod.core.a7.pos.DhLodPos;
public class ChunkSizedData extends FullArrayView {
public final byte dataDetail;
public final int x;
public final int z;
public ChunkSizedData(byte dataDetail, int x, int z) {
super(new IdBiomeBlockStateMap(), new long[16*16][0], 16);
this.dataDetail = dataDetail;
this.x = x;
this.z = z;
}
public void setSingleColumn(long[] data, int x, int z) {
dataArrays[x*16+z] = data;
}
public long nonEmptyCount() {
long count = 0;
for (long[] data : dataArrays) {
if (data.length != 0)
count += 1;
}
return count;
}
public long emptyCount() {
return 16*16 - nonEmptyCount();
}
public DhLodPos getBBoxLodPos() {
return new DhLodPos((byte) (dataDetail+4), x, z);
}
}
@@ -0,0 +1,14 @@
package com.seibel.lod.core.a7.datatype.full;
public enum EGenMode {
Empty,
Surface,
Feature,
Complete;
public static EGenMode get(byte genMode) {
return EGenMode.values()[genMode];
}
public static byte get(EGenMode genMode) {
return (byte) genMode.ordinal();
}
}
@@ -0,0 +1,111 @@
package com.seibel.lod.core.a7.datatype.full;
import com.seibel.lod.core.a7.datatype.LodDataSource;
import com.seibel.lod.core.a7.datatype.full.accessor.SingleFullArrayView;
import com.seibel.lod.core.a7.pos.DhLodPos;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.a7.save.io.file.IDataSourceProvider;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.util.LodUtil;
import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
public class FullDataDownSampler {
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public static CompletableFuture<LodDataSource> createDownSamplingFuture(DhSectionPos newTarget, IDataSourceProvider provider) {
// TODO: Make this future somehow run with lowest priority (to ensure ram usage stays low)
return createDownSamplingFuture(FullDataSource.createEmpty(newTarget), provider);
}
public static CompletableFuture<LodDataSource> createDownSamplingFuture(FullDataSource target, IDataSourceProvider provider) {
int sectionSizeNeeded = 1 << target.getDataDetail();
ArrayList<CompletableFuture<LodDataSource>> futures;
DhLodPos basePos = target.getSectionPos().getSectionBBoxPos().getCorner(FullDataSource.SECTION_SIZE_OFFSET);
if (sectionSizeNeeded <= FullDataSource.SECTION_SIZE_OFFSET) {
futures = new ArrayList<>(sectionSizeNeeded * sectionSizeNeeded);
for (int ox = 0; ox < sectionSizeNeeded; ox++) {
for (int oz = 0; oz < sectionSizeNeeded; oz++) {
CompletableFuture<LodDataSource> future = provider.read(new DhSectionPos(
FullDataSource.SECTION_SIZE_OFFSET, basePos.x + ox, basePos.z + oz));
future = future.whenComplete((source, ex) -> {
if (ex == null && source != null && source instanceof FullDataSource) {
downSample(target, (FullDataSource) source);
} else if (ex != null) {
LOGGER.error("Error while down sampling", ex);
}
});
futures.add(future);
}
}
} else {
futures = new ArrayList<>(FullDataSource.SECTION_SIZE * FullDataSource.SECTION_SIZE);
int multiplier = sectionSizeNeeded / FullDataSource.SECTION_SIZE;
for (int ox = 0; ox < FullDataSource.SECTION_SIZE; ox++) {
for (int oz = 0; oz < FullDataSource.SECTION_SIZE; oz++) {
CompletableFuture<LodDataSource> future = provider.read(new DhSectionPos(
FullDataSource.SECTION_SIZE_OFFSET, basePos.x + ox * multiplier, basePos.z + oz * multiplier));
future = future.whenComplete((source, ex) -> {
if (ex == null && source != null && source instanceof FullDataSource) {
downSample(target, (FullDataSource) source);
} else if (ex != null) {
LOGGER.error("Error while down sampling", ex);
}
});
futures.add(future);
}
}
}
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenApply(v -> target);
}
public static void downSample(FullDataSource target, FullDataSource source) {
LodUtil.assertTrue(target.getSectionPos().overlaps(source.getSectionPos()));
LodUtil.assertTrue(target.getDataDetail() > source.getDataDetail());
byte detailDiff = (byte) (target.getDataDetail() - source.getDataDetail());
DhSectionPos trgPos = target.getSectionPos();
DhSectionPos srcPos = source.getSectionPos();
if (detailDiff >= FullDataSource.SECTION_SIZE_OFFSET) {
// The source occupies only 1 datapoint in the target
// FIXME: TEMP method for down-sampling: take only the corner column
int sourceSectionPerTargetData = 1 << (detailDiff - FullDataSource.SECTION_SIZE_OFFSET);
if (srcPos.sectionX % sourceSectionPerTargetData != 0 || srcPos.sectionZ % sourceSectionPerTargetData != 0) {
return;
}
DhLodPos trgOffset = trgPos.getCorner(target.getDataDetail());
DhLodPos srcOffset = srcPos.getSectionBBoxPos().convertUpwardsTo(target.getDataDetail());
int offsetX = trgOffset.x - srcOffset.x;
int offsetZ = trgOffset.z - srcOffset.z;
LodUtil.assertTrue(offsetX >= 0 && offsetX < FullDataSource.SECTION_SIZE
&& offsetZ >= 0 && offsetZ < FullDataSource.SECTION_SIZE);
target.isEmpty = false;
source.get(0,0).deepCopyTo(target.get(offsetX, offsetZ));
} else if (detailDiff > 0) {
// The source occupies multiple data-points in the target
int srcDataPerTrgData = 1 << detailDiff;
int overlappedTrgDataSize = FullDataSource.SECTION_SIZE / srcDataPerTrgData;
DhLodPos trgOffset = trgPos.getCorner(target.getDataDetail());
DhLodPos srcOffset = srcPos.getSectionBBoxPos().getCorner(target.getDataDetail());
int offsetX = trgOffset.x - srcOffset.x;
int offsetZ = trgOffset.z - srcOffset.z;
LodUtil.assertTrue(offsetX >= 0 && offsetX < FullDataSource.SECTION_SIZE
&& offsetZ >= 0 && offsetZ < FullDataSource.SECTION_SIZE);
target.isEmpty = false;
for (int ox = 0; ox < overlappedTrgDataSize; ox++) {
for (int oz = 0; oz < overlappedTrgDataSize; oz++) {
SingleFullArrayView column = target.get(ox + offsetX, oz + offsetZ);
column.downsampleFrom(source.subView(srcDataPerTrgData, ox * srcDataPerTrgData, oz * srcDataPerTrgData));
}
}
} else {
LodUtil.assertNotReach();
}
}
}
@@ -0,0 +1,26 @@
package com.seibel.lod.core.a7.datatype.full;
import com.seibel.lod.core.a7.datatype.DataSourceLoader;
import com.seibel.lod.core.a7.datatype.LodDataSource;
import com.seibel.lod.core.a7.level.ILevel;
import com.seibel.lod.core.a7.save.io.file.DataMetaFile;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
public class FullDataLoader extends DataSourceLoader {
public FullDataLoader() {
super(FullDataSource.class, FullDataSource.TYPE_ID, new byte[]{FullDataSource.LATEST_VERSION});
}
@Override
public LodDataSource loadData(DataMetaFile dataFile, InputStream data, ILevel level) throws IOException {
try (
//TODO: Add decompressor here
DataInputStream dis = new DataInputStream(data);
) {
return FullDataSource.loadData(dataFile, dis, level);
}
}
}
@@ -0,0 +1,241 @@
package com.seibel.lod.core.a7.datatype.full;
import com.seibel.lod.core.a7.datatype.column.ColumnRenderSource;
import com.seibel.lod.core.a7.datatype.full.accessor.FullArrayView;
import com.seibel.lod.core.a7.datatype.full.accessor.SingleFullArrayView;
import com.seibel.lod.core.a7.level.ILevel;
import com.seibel.lod.core.a7.pos.DhBlockPos2D;
import com.seibel.lod.core.a7.pos.DhLodPos;
import com.seibel.lod.core.a7.save.io.file.DataMetaFile;
import com.seibel.lod.core.a7.datatype.LodDataSource;
import com.seibel.lod.core.a7.util.IdMappingUtil;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.objects.DHChunkPos;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.wrapperInterfaces.chunk.IChunkWrapper;
import org.apache.logging.log4j.Logger;
import java.io.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
public class FullDataSource extends FullArrayView implements LodDataSource { // 1 chunk
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public static final byte SECTION_SIZE_OFFSET = 6;
public static final int SECTION_SIZE = 1 << SECTION_SIZE_OFFSET;
public static final byte LATEST_VERSION = 0;
public static final long TYPE_ID = "FullDataSource".hashCode();
private final DhSectionPos sectionPos;
private int localVersion = 0;
public boolean isEmpty = true;
protected FullDataSource(DhSectionPos sectionPos) {
super(new IdBiomeBlockStateMap(), new long[SECTION_SIZE*SECTION_SIZE][0], SECTION_SIZE);
this.sectionPos = sectionPos;
}
@Override
public DhSectionPos getSectionPos() {
return sectionPos;
}
@Override
public byte getDataDetail() {
return (byte) (sectionPos.sectionDetail-SECTION_SIZE_OFFSET);
}
@Override
public void setLocalVersion(int localVer) {
localVersion = localVer;
}
@Override
public byte getDataVersion() {
return LATEST_VERSION;
}
@Override
public void update(ChunkSizedData data) {
LodUtil.assertTrue(sectionPos.getSectionBBoxPos().overlaps(data.getBBoxLodPos()));
if (data.dataDetail == 0 && getDataDetail() == 0) {
DhBlockPos2D chunkBlockPos = new DhBlockPos2D(data.x * 16, data.z * 16);
DhBlockPos2D blockOffset = chunkBlockPos.subtract(sectionPos.getCorner().getCorner());
LodUtil.assertTrue(blockOffset.x >= 0 && blockOffset.x < SECTION_SIZE && blockOffset.z >= 0 && blockOffset.z < SECTION_SIZE);
isEmpty = false;
data.shadowCopyTo(this.subView(16, blockOffset.x, blockOffset.z));
{ // DEBUG ASSERTION
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
SingleFullArrayView column = this.get(x + blockOffset.x, z + blockOffset.z);
LodUtil.assertTrue(column.doesItExist());
}
}
}
} else if (data.dataDetail == 0 && getDataDetail() < 4) {
int dataPerFull = 1 << getDataDetail();
int fullSize = 16 / dataPerFull;
DhLodPos dataOffset = data.getBBoxLodPos().getCorner(getDataDetail());
DhLodPos baseOffset = sectionPos.getCorner(getDataDetail());
int offsetX = dataOffset.x - baseOffset.x;
int offsetZ = dataOffset.z - baseOffset.z;
LodUtil.assertTrue(offsetX >= 0 && offsetX < SECTION_SIZE && offsetZ >= 0 && offsetZ < SECTION_SIZE);
isEmpty = false;
for (int ox = 0; ox < fullSize; ox++) {
for (int oz = 0; oz < fullSize; oz++) {
SingleFullArrayView column = this.get(ox + offsetX, oz + offsetZ);
column.downsampleFrom(data.subView(dataPerFull, ox * dataPerFull, oz * dataPerFull));
}
}
} else if (data.dataDetail == 0 && getDataDetail() >= 4) {
//FIXME: TEMPORARY
int chunkPerFull = 1 << (getDataDetail() - 4);
if (data.x % chunkPerFull != 0 || data.z % chunkPerFull != 0) return;
DhLodPos baseOffset = sectionPos.getCorner(getDataDetail());
DhLodPos dataOffset = data.getBBoxLodPos().convertUpwardsTo(getDataDetail());
int offsetX = dataOffset.x - baseOffset.x;
int offsetZ = dataOffset.z - baseOffset.z;
LodUtil.assertTrue(offsetX >= 0 && offsetX < SECTION_SIZE && offsetZ >= 0 && offsetZ < SECTION_SIZE);
isEmpty = false;
data.get(0,0).deepCopyTo(get(offsetX, offsetZ));
} else {
LodUtil.assertNotReach();
//TODO;
}
}
@Override
public void saveData(ILevel level, DataMetaFile file, OutputStream dataStream) throws IOException {
try (DataOutputStream dos = new DataOutputStream(dataStream)) {
dos.writeInt(getDataDetail());
dos.writeInt(size);
dos.writeInt(level.getMinY());
if (isEmpty) {
dos.writeInt(0x00000001);
return;
}
dos.writeInt(0xFFFFFFFF);
// Data array length
for (int x = 0; x < size; x++) {
for (int z = 0; z < size; z++) {
dos.writeByte(get(x, z).getSingleLength());
}
}
// Data array content (only on non-empty columns)
dos.writeInt(0xFFFFFFFF);
for (int x = 0; x < size; x++) {
for (int z = 0; z < size; z++) {
SingleFullArrayView column = get(x, z);
if (!column.doesItExist()) continue;
long[] raw = column.getRaw();
for (long l : raw) {
dos.writeLong(l);
}
}
}
// Id mapping
dos.writeInt(0xFFFFFFFF);
mapping.serialize(dos);
dos.writeInt(0xFFFFFFFF);
}
}
public static FullDataSource loadData(DataMetaFile dataFile, InputStream dataStream, ILevel level) throws IOException {
try (DataInputStream dos = new DataInputStream(dataStream)) {
int dataDetail = dos.readInt();
if(dataDetail != dataFile.dataLevel)
throw new IOException(LodUtil.formatLog("Data level mismatch: {} != {}", dataDetail, dataFile.dataLevel));
int size = dos.readInt();
if (size != SECTION_SIZE)
throw new IOException(LodUtil.formatLog(
"Section size mismatch: {} != {} (Currently only 1 section size is supported)", size, SECTION_SIZE));
int minY = dos.readInt();
if (minY != level.getMinY())
LOGGER.warn("Data minY mismatch: {} != {}. Will ignore data's y level", minY, level.getMinY());
int end = dos.readInt();
// Data array length
if (end == 0x00000001) {
// Section is empty
return new FullDataSource(dataFile.pos);
}
// Non-empty section
if (end != 0xFFFFFFFF) throw new IOException("invalid header end guard");
long[][] data = new long[size*size][];
for (int x = 0; x < size; x++) {
for (int z = 0; z < size; z++) {
data[x*size+z] = new long[dos.readByte()];
}
}
// Data array content (only on non-empty columns)
end = dos.readInt();
if (end != 0xFFFFFFFF) throw new IOException("invalid data length end guard");
for (int i = 0; i < data.length; i++) {
if (data[i].length == 0) continue;
for (int j = 0; j < data[i].length; j++) {
data[i][j] = dos.readLong();
}
}
// Id mapping
end = dos.readInt();
if (end != 0xFFFFFFFF) throw new IOException("invalid data content end guard");
IdBiomeBlockStateMap mapping = IdBiomeBlockStateMap.deserialize(dos);
end = dos.readInt();
if (end != 0xFFFFFFFF) throw new IOException("invalid id mapping end guard");
return new FullDataSource(dataFile.pos, mapping, data);
}
}
private FullDataSource(DhSectionPos pos, IdBiomeBlockStateMap mapping, long[][] data) {
super(mapping, data, SECTION_SIZE);
LodUtil.assertTrue(data.length == SECTION_SIZE*SECTION_SIZE);
this.sectionPos = pos;
isEmpty = false;
}
public static FullDataSource createEmpty(DhSectionPos pos) {
return new FullDataSource(pos);
}
public static boolean neededForPosition(DhSectionPos posToWrite, DhSectionPos posToTest) {
if (!posToWrite.overlaps(posToTest)) return false;
if (posToTest.sectionDetail > posToWrite.sectionDetail) return false;
if (posToWrite.sectionDetail - posToTest.sectionDetail <= SECTION_SIZE_OFFSET) return true;
byte sectPerData = (byte) (1 << (posToWrite.sectionDetail - posToTest.sectionDetail - SECTION_SIZE_OFFSET));
return posToTest.sectionX % sectPerData == 0 && posToTest.sectionZ % sectPerData == 0;
}
public void writeFromLower(FullDataSource subData) {
LodUtil.assertTrue(sectionPos.overlaps(subData.sectionPos));
LodUtil.assertTrue(subData.sectionPos.sectionDetail < sectionPos.sectionDetail);
if (!neededForPosition(sectionPos, subData.sectionPos)) return;
DhSectionPos lowerSectPos = subData.sectionPos;
byte detailDiff = (byte) (sectionPos.sectionDetail - subData.sectionPos.sectionDetail);
byte targetDataDetail = getDataDetail();
DhLodPos minDataPos = sectionPos.getCorner(targetDataDetail);
if (detailDiff <= SECTION_SIZE_OFFSET) {
int count = 1 << detailDiff;
int dataPerCount = SECTION_SIZE / count;
DhLodPos subDataPos = lowerSectPos.getSectionBBoxPos().getCorner(targetDataDetail);
int dataOffsetX = subDataPos.x - minDataPos.x;
int dataOffsetZ = subDataPos.z - minDataPos.z;
LodUtil.assertTrue(dataOffsetX >= 0 && dataOffsetX < SECTION_SIZE && dataOffsetZ >= 0 && dataOffsetZ < SECTION_SIZE);
for (int ox = 0; ox < count; ox++) {
for (int oz = 0; oz < count; oz++) {
SingleFullArrayView column = this.get(ox + dataOffsetX, oz + dataOffsetZ);
column.downsampleFrom(subData.subView(dataPerCount, ox * dataPerCount, oz * dataPerCount));
}
}
} else {
// Count == 1
DhLodPos subDataPos = lowerSectPos.getSectionBBoxPos().convertUpwardsTo(targetDataDetail);
int dataOffsetX = subDataPos.x - minDataPos.x;
int dataOffsetZ = subDataPos.z - minDataPos.z;
LodUtil.assertTrue(dataOffsetX >= 0 && dataOffsetX < SECTION_SIZE && dataOffsetZ >= 0 && dataOffsetZ < SECTION_SIZE);
subData.get(0,0).deepCopyTo(get(dataOffsetX, dataOffsetZ));
}
}
}
@@ -0,0 +1,78 @@
package com.seibel.lod.core.a7.datatype.full;
// Static class for the data format:
// ID: blockState id Y: Height(signed) DP: Depth(signed?) (Depth means the length of the block!)
// BL: Block light SL: Sky light
// =======Bit layout=======
// BL BL BL BL SL SL SL SL <-- Top bits
// YY YY YY YY YY YY YY YY
// YY YY YY YY DP DP DP DP
// DP DP DP DP DP DP DP DP
// ID ID ID ID ID ID IO ID
// ID ID ID ID ID ID IO ID
// ID ID ID ID ID ID IO ID
// ID ID ID ID ID ID IO ID <-- Bottom bits
import com.seibel.lod.core.util.LodUtil;
import org.jetbrains.annotations.Contract;
import static com.seibel.lod.core.a7.datatype.column.accessor.ColumnFormat.MAX_WORLD_Y_SIZE;
public class FullFormat {
public static final int ID_WIDTH = 32;
public static final int DP_WIDTH = 12;
public static final int Y_WIDTH = 12;
public static final int LIGHT_WIDTH = 8;
public static final int ID_OFFSET = 0;
public static final int DP_OFFSET = ID_OFFSET + ID_WIDTH;
public static final int Y_OFFSET = DP_OFFSET + DP_WIDTH;
public static final int LIGHT_OFFSET = Y_OFFSET + Y_WIDTH;
public static final long ID_MASK = Integer.MAX_VALUE;
public static final long INVERSE_ID_MASK = ~ID_MASK;
public static final int DP_MASK = (int)Math.pow(2, DP_WIDTH) - 1;
public static final int Y_MASK = (int)Math.pow(2, Y_WIDTH) - 1;
public static final int LIGHT_MASK = (int)Math.pow(2, LIGHT_WIDTH) - 1;
public static long encode(int id, int depth, int y, byte lightPair) {
LodUtil.assertTrue(y >= 0 && y < MAX_WORLD_Y_SIZE, "Trying to create datapoint with y[{}] out of range!", y);
LodUtil.assertTrue(depth > 0 && depth < MAX_WORLD_Y_SIZE, "Trying to create datapoint with depth[{}] out of range!", depth);
LodUtil.assertTrue(y+depth <= MAX_WORLD_Y_SIZE, "Trying to create datapoint with y+depth[{}] out of range!", y+depth);
long data = 0;
data |= id & ID_MASK;
data |= (long) (depth & DP_MASK) << DP_OFFSET;
data |= (long) (y & Y_MASK) << Y_OFFSET;
data |= (long) lightPair << LIGHT_OFFSET;
LodUtil.assertTrue(getId(data) == id && getDepth(data) == depth && getY(data) == y && getLight(data) == Byte.toUnsignedInt(lightPair),
"Trying to create datapoint with id[{}], depth[{}], y[{}], lightPair[{}] but got id[{}], depth[{}], y[{}], lightPair[{}]!",
id, depth, y, Byte.toUnsignedInt(lightPair), getId(data), getDepth(data), getY(data), getLight(data));
return data;
}
public static int getId(long data) {
return (int) (data & ID_MASK);
}
public static int getDepth(long data) {
return (int) ((data >> DP_OFFSET) & DP_MASK);
}
public static int getY(long data) {
return (int) ((data >> Y_OFFSET) & Y_MASK);
}
public static int getLight(long data) {
return (int) ((data >> LIGHT_OFFSET) & LIGHT_MASK);
}
public static String toString(long data) {
return "[ID:" + getId(data) + ",Y:" + getY(data) + ",Depth:" + getY(data) + ",Light:" + getLight(data) + "]";
}
@Contract(pure = true)
public static long remap(int[] mapping, long data) {
return (data & INVERSE_ID_MASK) | mapping[(int)data];
}
}
@@ -0,0 +1,115 @@
package com.seibel.lod.core.a7.datatype.full;
import com.seibel.lod.core.handlers.dependencyInjection.SingletonInjector;
import com.seibel.lod.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.lod.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.IBiomeWrapper;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Objects;
// WARNING: This is not THREAD-SAFE!
public class IdBiomeBlockStateMap {
public static final IWrapperFactory FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
public static final class Entry {
public final IBiomeWrapper biome;
public final IBlockStateWrapper blockState;
public Entry(IBiomeWrapper biome, IBlockStateWrapper blockState) {
this.biome = biome;
this.blockState = blockState;
}
@Override
public int hashCode() {
return Objects.hash(biome, blockState);
}
@Override
public boolean equals(Object other) {
if (other == this) return true;
if (!(other instanceof Entry)) return false;
return ((Entry) other).biome.equals(biome) && ((Entry) other).blockState.equals(blockState);
}
public String serialize() {
return biome.serialize() + " " + blockState.serialize();
}
public static Entry deserialize(String str) throws IOException {
String[] strs = str.split(" ");
if (strs.length != 2) throw new IOException("Failed to deserialize BiomeBlockStateEntry");
IBiomeWrapper biome = FACTORY.deserializeBiomeWrapper(strs[0]);
IBlockStateWrapper blockState = FACTORY.deserializeBlockStateWrapper(strs[1]);
return new Entry(biome, blockState);
}
}
final ArrayList<Entry> entries = new ArrayList<>();
final HashMap<Entry, Integer> idMap = new HashMap<>();
public Entry get(int id) {
return entries.get(id);
}
public int setAndGetId(IBiomeWrapper biome, IBlockStateWrapper blockState) {
return idMap.computeIfAbsent(new Entry(biome, blockState), (e) -> {
int id = entries.size();
entries.add(e);
return id;
});
}
public int setAndGetId(Entry biomeBlockStateEntry) {
return idMap.computeIfAbsent(biomeBlockStateEntry, (e) -> {
int id = entries.size();
entries.add(e);
return id;
});
}
public int[] computeAndMergeMapFrom(IdBiomeBlockStateMap target) {
ArrayList<Entry> mergeEntry = target.entries;
int[] mapper = new int[mergeEntry.size()];
for (int i=0; i<mergeEntry.size(); i++) {
mapper[i] = setAndGetId(mergeEntry.get(i));
}
return mapper;
}
void serialize(OutputStream os) {
try (DataOutputStream dos = new DataOutputStream(os)) {
dos.writeInt(entries.size());
for (Entry e : entries) {
dos.writeUTF(e.serialize());
}
} catch (IOException e) {
e.printStackTrace();
}
}
static IdBiomeBlockStateMap deserialize(InputStream is) {
try (DataInputStream dis = new DataInputStream(is)) {
int size = dis.readInt();
IdBiomeBlockStateMap map = new IdBiomeBlockStateMap();
for (int i = 0; i < size; i++) {
map.entries.add(Entry.deserialize(dis.readUTF()));
}
return map;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
@Override
public boolean equals(Object other) {
if (other == this) return true;
if (!(other instanceof IdBiomeBlockStateMap)) return false;
IdBiomeBlockStateMap otherMap = (IdBiomeBlockStateMap) other;
if (entries.size() != otherMap.entries.size()) return false;
for (int i=0; i<entries.size(); i++) {
if (!entries.get(i).equals(otherMap.entries.get(i))) return false;
}
return true;
}
}
@@ -0,0 +1,12 @@
package com.seibel.lod.core.a7.datatype.full;
import com.seibel.lod.core.a7.pos.DhSectionPos;
public class SampledDataSource extends FullDataSource {
private boolean[] isGenerated;
protected SampledDataSource(DhSectionPos sectionPos) {
super(sectionPos);
}
}
@@ -0,0 +1,26 @@
package com.seibel.lod.core.a7.datatype.full;
import com.seibel.lod.core.a7.datatype.DataSourceLoader;
import com.seibel.lod.core.a7.datatype.LodDataSource;
import com.seibel.lod.core.a7.level.ILevel;
import com.seibel.lod.core.a7.save.io.file.DataMetaFile;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
public class SparseDataLoader extends DataSourceLoader {
public SparseDataLoader() {
super(SparseDataSource.class, SparseDataSource.TYPE_ID, new byte[]{SparseDataSource.LATEST_VERSION});
}
@Override
public LodDataSource loadData(DataMetaFile dataFile, InputStream data, ILevel level) throws IOException {
try (
//TODO: Add decompressor here
DataInputStream dis = new DataInputStream(data);
) {
return SparseDataSource.loadData(dataFile, dis, level);
}
}
}
@@ -0,0 +1,248 @@
package com.seibel.lod.core.a7.datatype.full;
import com.seibel.lod.core.a7.datatype.LodDataSource;
import com.seibel.lod.core.a7.datatype.full.accessor.FullArrayView;
import com.seibel.lod.core.a7.datatype.full.accessor.SingleFullArrayView;
import com.seibel.lod.core.a7.level.ILevel;
import com.seibel.lod.core.a7.pos.DhLodPos;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.a7.save.io.file.DataMetaFile;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.util.LodUtil;
import org.apache.logging.log4j.Logger;
import java.io.*;
import java.util.BitSet;
public class SparseDataSource implements LodDataSource {
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public static final byte SPARSE_UNIT_DETAIL = 4;
public static final byte SPARSE_UNIT_SIZE = 1 << SPARSE_UNIT_DETAIL;
public static final byte SECTION_SIZE_OFFSET = 6;
public static final int SECTION_SIZE = 1 << SECTION_SIZE_OFFSET;
public static final byte MAX_SECTION_DETAIL = SECTION_SIZE_OFFSET + SPARSE_UNIT_DETAIL;
public static final byte LATEST_VERSION = 0;
public static final long TYPE_ID = "SparseDataSource".hashCode();
protected final IdBiomeBlockStateMap mapping;
private final DhSectionPos sectionPos;
private final FullArrayView[] sparseData;
private final int chunks;
private final int dataPerChunk;
private final DhLodPos chunkPos;
public boolean isEmpty = true;
public static SparseDataSource createEmpty(DhSectionPos pos) {
return new SparseDataSource(pos);
}
protected SparseDataSource(DhSectionPos sectionPos) {
LodUtil.assertTrue(sectionPos.sectionDetail > SPARSE_UNIT_DETAIL);
LodUtil.assertTrue(sectionPos.sectionDetail <= MAX_SECTION_DETAIL);
this.sectionPos = sectionPos;
chunks = 1 << (byte) (sectionPos.sectionDetail - SPARSE_UNIT_DETAIL);
dataPerChunk = SECTION_SIZE / chunks;
sparseData = new FullArrayView[chunks * chunks];
chunkPos = sectionPos.getCorner(SPARSE_UNIT_DETAIL);
mapping = new IdBiomeBlockStateMap();
}
protected SparseDataSource(DhSectionPos sectionPos, IdBiomeBlockStateMap mapping, FullArrayView[] data) {
LodUtil.assertTrue(sectionPos.sectionDetail > SPARSE_UNIT_DETAIL);
LodUtil.assertTrue(sectionPos.sectionDetail <= MAX_SECTION_DETAIL);
this.sectionPos = sectionPos;
chunks = 1 << (byte) (sectionPos.sectionDetail - SPARSE_UNIT_DETAIL);
dataPerChunk = SECTION_SIZE / chunks;
LodUtil.assertTrue(chunks*chunks == data.length);
sparseData = data;
chunkPos = sectionPos.getCorner(SPARSE_UNIT_DETAIL);
this.mapping = mapping;
}
@Override
public DhSectionPos getSectionPos() {
return sectionPos;
}
@Override
public byte getDataDetail() {
return (byte) (sectionPos.sectionDetail-SECTION_SIZE_OFFSET);
}
@Override
public void setLocalVersion(int localVer) {
//TODO: implement
}
@Override
public byte getDataVersion() {
return LATEST_VERSION;
}
private int calculateOffset(int cx, int cz) {
int ox = cx - chunkPos.x;
int oz = cz - chunkPos.z;
LodUtil.assertTrue(ox >= 0 && oz >= 0 && ox < chunks && oz < chunks);
return ox * chunks + oz;
}
@Override
public void update(ChunkSizedData data) {
if (data.dataDetail != 0) {
//TODO: Disable the throw and instead just ignore the data.
throw new IllegalArgumentException("SparseDataSource only supports dataDetail 0!");
}
int arrayOffset = calculateOffset(data.x, data.z);
FullArrayView newArray = new FullArrayView(mapping, new long[dataPerChunk * dataPerChunk][], dataPerChunk);
if (getDataDetail() == data.dataDetail) {
data.shadowCopyTo(newArray);
} else {
int count = dataPerChunk;
int dataPerCount = SPARSE_UNIT_SIZE / dataPerChunk;
for (int ox = 0; ox < count; ox++) {
for (int oz = 0; oz < count; oz++) {
SingleFullArrayView column = newArray.get(ox, oz);
column.downsampleFrom(data.subView(dataPerCount, ox * dataPerCount, oz * dataPerCount));
}
}
}
sparseData[arrayOffset] = newArray;
}
@Override
public void saveData(ILevel level, DataMetaFile file, OutputStream dataStream) throws IOException {
try (DataOutputStream dos = new DataOutputStream(dataStream)) {
dos.writeShort(getDataDetail());
dos.writeShort(SPARSE_UNIT_DETAIL);
dos.writeInt(SECTION_SIZE);
dos.writeInt(level.getMinY());
if (isEmpty) {
dos.writeInt(0x00000001);
return;
}
dos.writeInt(0xFFFFFFFF);
// sparse array existence bitset
BitSet set = new BitSet(sparseData.length);
for (int i = 0; i < sparseData.length; i++) set.set(i, sparseData[i] != null);
byte[] bytes = set.toByteArray();
dos.writeInt(bytes.length);
dos.write(bytes);
// Data array content (only on non-empty stuff)
dos.writeInt(0xFFFFFFFF);
for (int i = set.nextSetBit(0); i >= 0; i = set.nextSetBit(i + 1)) {
FullArrayView array = sparseData[i];
LodUtil.assertTrue(array != null);
for (int x = 0; x < array.width(); x++) {
for (int z = 0; z < array.width(); z++) {
dos.writeByte(array.get(x, z).getSingleLength());
}
}
for (int x = 0; x < array.width(); x++) {
for (int z = 0; z < array.width(); z++) {
SingleFullArrayView column = array.get(x, z);
LodUtil.assertTrue(column.getMapping() == mapping); //MUST be exact equal!
if (!column.doesItExist()) continue;
long[] raw = column.getRaw();
for (long l : raw) {
dos.writeLong(l);
}
}
}
}
// Id mapping
dos.writeInt(0xFFFFFFFF);
mapping.serialize(dos);
dos.writeInt(0xFFFFFFFF);
}
}
public static SparseDataSource loadData(DataMetaFile dataFile, InputStream dataStream, ILevel level) throws IOException {
LodUtil.assertTrue(dataFile.pos.sectionDetail > SPARSE_UNIT_DETAIL);
LodUtil.assertTrue(dataFile.pos.sectionDetail <= MAX_SECTION_DETAIL);
try (DataInputStream dos = new DataInputStream(dataStream)) {
int dataDetail = dos.readShort();
if(dataDetail != dataFile.dataLevel)
throw new IOException(LodUtil.formatLog("Data level mismatch: {} != {}", dataDetail, dataFile.dataLevel));
int sparseDetail = dos.readShort();
if (sparseDetail != SPARSE_UNIT_DETAIL)
throw new IOException((LodUtil.formatLog("Unexpected sparse detail level: {} != {}",
sparseDetail, SPARSE_UNIT_DETAIL)));
int size = dos.readInt();
if (size != SECTION_SIZE)
throw new IOException(LodUtil.formatLog(
"Section size mismatch: {} != {} (Currently only 1 section size is supported)", size, SECTION_SIZE));
int chunks = 1 << (byte) (dataFile.pos.sectionDetail - sparseDetail);
int dataPerChunk = size / chunks;
int minY = dos.readInt();
if (minY != level.getMinY())
LOGGER.warn("Data minY mismatch: {} != {}. Will ignore data's y level", minY, level.getMinY());
int end = dos.readInt();
// Data array length
if (end == 0x00000001) {
// Section is empty
return createEmpty(dataFile.pos);
}
// Non-empty section
if (end != 0xFFFFFFFF) throw new IOException("invalid header end guard");
int length = dos.readInt();
if (length <= 0 || length > chunks*chunks/8+64)
throw new IOException(LodUtil.formatLog("Sparse Flag BitSet size outside reasonable range: {} (expects {} to {})",
length, 1, chunks*chunks/8+63));
byte[] bytes = dos.readNBytes(length);
BitSet set = BitSet.valueOf(bytes);
if (set.size() < chunks*chunks)
throw new IOException((LodUtil.formatLog("Sparse Flag BitSet too small: {} != {}*{}",
set.size(), chunks, chunks)));
long[][][] dataChunks = new long[chunks*chunks][][];
// Data array content (only on non-empty columns)
end = dos.readInt();
if (end != 0xFFFFFFFF) throw new IOException("invalid data length end guard");
for (int i = set.nextSetBit(0); i >= 0 && i < dataChunks.length; i = set.nextSetBit(i + 1)) {
long[][] dataColumns = new long[dataPerChunk*dataPerChunk][];
dataChunks[i] = dataColumns;
for (int j = 0; j < dataColumns.length; j++) {
dataColumns[i] = new long[dos.readByte()];
}
for (int k = 0; k < dataColumns.length; k++) {
if (dataColumns[k].length == 0) continue;
for (int o = 0; o < dataColumns[k].length; o++) {
dataColumns[k][o] = dos.readLong();
}
}
}
// Id mapping
end = dos.readInt();
if (end != 0xFFFFFFFF) throw new IOException("invalid data content end guard");
IdBiomeBlockStateMap mapping = IdBiomeBlockStateMap.deserialize(dos);
end = dos.readInt();
if (end != 0xFFFFFFFF) throw new IOException("invalid id mapping end guard");
FullArrayView[] objectChunks = new FullArrayView[chunks*chunks];
for (int i=0; i<dataChunks.length; i++) {
if (dataChunks[i] == null) continue;
objectChunks[i] = new FullArrayView(mapping, new long[dataPerChunk * dataPerChunk][], dataPerChunk);
}
return new SparseDataSource(dataFile.pos, mapping, objectChunks);
}
}
public void applyToFullDataSource(FullDataSource dataSource) {
LodUtil.assertTrue(dataSource.getSectionPos().equals(sectionPos));
LodUtil.assertTrue(dataSource.getDataDetail() == getDataDetail());
for (int x = 0; x<chunks; x++) {
for (int z = 0; z<chunks; z++) {
FullArrayView array = sparseData[x*chunks+z];
if (array == null) continue;
// Otherwise, apply data to dataSource
FullArrayView view = dataSource.subView(dataPerChunk, x*dataPerChunk, z*dataPerChunk);
array.shadowCopyTo(view);
}
}
}
}
@@ -0,0 +1,83 @@
package com.seibel.lod.core.a7.datatype.full.accessor;
import com.seibel.lod.core.a7.datatype.full.FullFormat;
import com.seibel.lod.core.a7.datatype.full.IdBiomeBlockStateMap;
public class FullArrayView implements IFullDataView {
protected final long[][] dataArrays;
protected final int offset;
protected final int size;
protected final int dataSize;
protected final IdBiomeBlockStateMap mapping;
public FullArrayView(IdBiomeBlockStateMap mapping, long[][] dataArrays, int size) {
if (dataArrays.length != size*size)
throw new IllegalArgumentException(
"tried constructing dataArrayView with invalid input!");
this.dataArrays = dataArrays;
this.size = size;
this.dataSize = size;
this.mapping = mapping;
offset = 0;
}
public FullArrayView(FullArrayView source, int size, int offsetX, int offsetZ) {
if (source.size < size || source.size < size+offsetX || source.size < size+offsetZ)
throw new IllegalArgumentException(
"tried constructing dataArrayView subview with invalid input!");
dataArrays = source.dataArrays;
this.size = size;
this.dataSize = source.dataSize;
mapping = source.mapping;
offset = source.offset + offsetX * dataSize + offsetZ;
}
@Override
public IdBiomeBlockStateMap getMapping() {
return mapping;
}
@Override
public SingleFullArrayView get(int index) {
return get(index/size, index%size);
}
@Override
public SingleFullArrayView get(int x, int z) {
return new SingleFullArrayView(mapping, dataArrays, x*size + z + offset);
}
@Override
public int width() {
return size;
}
@Override
public FullArrayView subView(int size, int ox, int oz) {
return new FullArrayView(this, size, ox, oz);
}
//WARNING: It will potentially share the underlying array object!
public void shadowCopyTo(FullArrayView target) {
if (target.size != size)
throw new IllegalArgumentException("Target view must have same size as this view");
if (target.mapping.equals(mapping)) {
for (int x = 0; x < size; x++) {
System.arraycopy(dataArrays, offset + x * dataSize,
target.dataArrays, target.offset + x * target.dataSize, size);
}
}
else {
int[] map = target.mapping.computeAndMergeMapFrom(mapping);
for (int x = 0; x < size; x++) {
for (int o=0; o<size; o++) {
long[] sourceData = dataArrays[offset + x * dataSize + o];
long[] newData = new long[sourceData.length];
for (int i = 0; i < newData.length; i++) {
newData[i] = FullFormat.remap(map, sourceData[i]);
}
target.dataArrays[target.offset + x * target.dataSize + o] = newData;
}
}
}
}
}
@@ -0,0 +1,13 @@
package com.seibel.lod.core.a7.datatype.full.accessor;
public interface IFullDataType {
byte getDetailOffset();
default int getDataSize() {
return 1 << getDetailOffset();
}
long getRoughRamUsage();
int getVerticalSize(int posX, int posZ);
boolean doesItExist(int posX, int posZ);
byte getGenModeAtChunk(int chunkX, int chunkZ);
SingleFullArrayView getDataAtColumn(int posX, int posZ);
}
@@ -0,0 +1,31 @@
package com.seibel.lod.core.a7.datatype.full.accessor;
import com.seibel.lod.core.a7.datatype.full.IdBiomeBlockStateMap;
import com.seibel.lod.core.util.LodUtil;
import java.util.Iterator;
public interface IFullDataView {
IdBiomeBlockStateMap getMapping();
SingleFullArrayView get(int index);
SingleFullArrayView get(int x, int z);
int width();
default Iterator<SingleFullArrayView> iterator() {
return new Iterator<SingleFullArrayView>() {
private int index = 0;
private final int size = width()*width();
@Override
public boolean hasNext() {
return index < size;
}
@Override
public SingleFullArrayView next() {
LodUtil.assertTrue(hasNext(), "No more data to iterate!");
return get(index++);
}
};
}
IFullDataView subView(int size, int ox, int oz);
}
@@ -0,0 +1,99 @@
package com.seibel.lod.core.a7.datatype.full.accessor;
import com.seibel.lod.core.a7.datatype.full.FullFormat;
import com.seibel.lod.core.a7.datatype.full.IdBiomeBlockStateMap;
public class SingleFullArrayView implements IFullDataView {
private final long[][] dataArrays;
private final int offset;
private final IdBiomeBlockStateMap mapping;
public SingleFullArrayView(IdBiomeBlockStateMap mapping, long[][] dataArrays, int offset) {
this.dataArrays = dataArrays;
this.offset = offset;
this.mapping = mapping;
}
public boolean doesItExist() {
return dataArrays[offset].length!=0;
}
@Override
public IdBiomeBlockStateMap getMapping() {
return mapping;
}
@Override
public SingleFullArrayView get(int index) {
if (index != 0) throw new IllegalArgumentException("Only contains 1 column of full data!");
return this;
}
@Override
public SingleFullArrayView get(int x, int z) {
if (x != 0 || z != 0) throw new IllegalArgumentException("Only contains 1 column of full data!");
return this;
}
public long[] getRaw() {
return dataArrays[offset];
}
public long getSingle(int yIndex) {
return dataArrays[offset][yIndex];
}
public void setSingle(int yIndex, long value) {
dataArrays[offset][yIndex] = value;
}
public void setNew(long[] newArray) {
dataArrays[offset] = newArray;
}
public int getSingleLength() { return dataArrays[offset].length; }
@Override
public int width() {
return 1;
}
@Override
public IFullDataView subView(int size, int ox, int oz) {
if (size != 1 || ox != 1 || oz != 1)
throw new IllegalArgumentException("Getting invalid range of subView from SingleFullArrayView!");
return this;
}
//WARNING: It may potentially share the underlying array object!
public void shadowCopyTo(SingleFullArrayView target) {
if (target.mapping.equals(mapping)) {
target.dataArrays[target.offset] = dataArrays[offset];
}
else {
int[] map = target.mapping.computeAndMergeMapFrom(mapping);
long[] sourceData = dataArrays[offset];
long[] newData = new long[sourceData.length];
for (int i = 0; i < newData.length; i++) {
newData[i] = FullFormat.remap(map, sourceData[i]);
}
target.dataArrays[target.offset] = newData;
}
}
public void deepCopyTo(SingleFullArrayView target) {
if (target.mapping.equals(mapping)) {
target.dataArrays[target.offset] = dataArrays[offset].clone();
}
else {
int[] map = target.mapping.computeAndMergeMapFrom(mapping);
long[] sourceData = dataArrays[offset];
long[] newData = new long[sourceData.length];
for (int i = 0; i < newData.length; i++) {
newData[i] = FullFormat.remap(map, sourceData[i]);
}
target.dataArrays[target.offset] = newData;
}
}
public void downsampleFrom(IFullDataView source) {
//TODO: Temp downsample method
SingleFullArrayView firstColumn = source.get(0);
firstColumn.deepCopyTo(this);
}
}
@@ -0,0 +1,31 @@
package com.seibel.lod.core.a7.datatype.transform;
import com.seibel.lod.core.a7.datatype.LodDataSource;
import com.seibel.lod.core.a7.datatype.LodRenderSource;
import com.seibel.lod.core.a7.datatype.column.ColumnRenderLoader;
import com.seibel.lod.core.a7.datatype.column.ColumnRenderSource;
import com.seibel.lod.core.a7.level.IClientLevel;
import com.seibel.lod.core.util.LodUtil;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
//TODO: Merge this with FullToColumnTransformer
public class DataRenderTransformer {
public static final ExecutorService TRANSFORMER_THREADS
= LodUtil.makeSingleThreadPool("Data/Render Transformer");
public static CompletableFuture<LodRenderSource> transformDataSource(LodDataSource data, IClientLevel level) {
return CompletableFuture.supplyAsync(() -> transform(data, level), TRANSFORMER_THREADS);
}
public static CompletableFuture<LodRenderSource> asyncTransformDataSource(CompletableFuture<LodDataSource> data, IClientLevel level) {
return data.thenApplyAsync((d) -> transform(d, level), TRANSFORMER_THREADS);
}
private static LodRenderSource transform(LodDataSource dataSource, IClientLevel level) {
if (dataSource == null) return null;
return ColumnRenderLoader.loaderRegistry.get(ColumnRenderSource.class)
.stream().findFirst().get().createRender(dataSource, level);
}
}
@@ -0,0 +1,412 @@
package com.seibel.lod.core.a7.datatype.transform;
import com.seibel.lod.core.a7.datatype.column.accessor.ColumnFormat;
import com.seibel.lod.core.a7.datatype.column.ColumnRenderSource;
import com.seibel.lod.core.a7.datatype.column.accessor.ColumnArrayView;
import com.seibel.lod.core.a7.datatype.column.accessor.ColumnQuadView;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.datatype.full.FullDataSource;
import com.seibel.lod.core.a7.datatype.full.FullFormat;
import com.seibel.lod.core.a7.datatype.full.IdBiomeBlockStateMap;
import com.seibel.lod.core.a7.datatype.full.accessor.SingleFullArrayView;
import com.seibel.lod.core.a7.level.IClientLevel;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.config.Config;
import com.seibel.lod.core.handlers.dependencyInjection.SingletonInjector;
import com.seibel.lod.core.objects.DHBlockPos;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.lod.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.IBiomeWrapper;
public class FullToColumnTransformer {
private static final IBlockStateWrapper AIR = SingletonInjector.INSTANCE.get(IWrapperFactory.class).getAirBlockStateWrapper();
/**
* Creates a LodNode for a chunk in the given world.
* @throws IllegalArgumentException thrown if either the chunk or world is null.
*/
public static ColumnRenderSource transformFullDataToColumnData(IClientLevel level, FullDataSource data) {
final DhSectionPos pos = data.getSectionPos();
final byte dataDetail = data.getDataDetail();
final int vertSize = Config.Client.Graphics.Quality.verticalQuality.get().calculateMaxVerticalData(data.getDataDetail());
final ColumnRenderSource columnSource = new ColumnRenderSource(pos, vertSize, level.getMinY());
if (data.isEmpty) return columnSource;
columnSource.isEmpty = false;
if (dataDetail == columnSource.getDataDetail()) {
int baseX = pos.getCorner().getCorner().x;
int baseZ = pos.getCorner().getCorner().z;
for (int x = 0; x < pos.getWidth(dataDetail).value; x++) {
for (int z = 0; z < pos.getWidth(dataDetail).value; z++) {
ColumnArrayView columnArrayView = columnSource.getVerticalDataView(x, z);
SingleFullArrayView fullArrayView = data.get(x, z);
convertColumnData(level, baseX + x, baseZ + z, columnArrayView, fullArrayView);
if (fullArrayView.doesItExist()) LodUtil.assertTrue(columnSource.doesItExist(x, z));
}
}
// } else if (dataDetail == 0 && columnSource.getDataDetail() > dataDetail) {
// byte deltaDetail = (byte) (columnSource.getDataDetail() - dataDetail);
// int perColumnWidth = 1 << deltaDetail;
// int columnCount = pos.getWidth(dataDetail).value / perColumnWidth;
//
//
// for (int x = 0; x < pos.getWidth(dataDetail).value; x++) {
// for (int z = 0; z < pos.getWidth(dataDetail).value; z++) {
// ColumnArrayView columnArrayView = columnSource.getVerticalDataView(x, z);
// SingleFullArrayView fullArrayView = data.get(x, z);
// convertColumnData(level, columnArrayView, fullArrayView);
// }
// }
} else {
throw new UnsupportedOperationException("To be implemented");
//FIXME: Implement different size creation of renderData
}
return columnSource;
}
public static void writeFullDataChunkToColumnData(ColumnRenderSource render, IClientLevel level, ChunkSizedData data) {
if (data.dataDetail != 0)
throw new UnsupportedOperationException("To be implemented");
final DhSectionPos pos = render.getSectionPos();
final int renderOffsetX = (data.x*16) - pos.getCorner().getCorner().x;
final int renderOffsetZ = (data.z*16) - pos.getCorner().getCorner().z;
final int blockX = pos.getCorner().getCorner().x;
final int blockZ = pos.getCorner().getCorner().z;
final int perRenderWidth = 1 << render.getDataDetail();
final int perDataWidth = 1 << data.dataDetail;
if (data.dataDetail == render.getDataDetail()) {
if (renderOffsetX < 0 || renderOffsetX+16 > render.getDataSize() || renderOffsetZ < 0 || renderOffsetZ+16 > render.getDataSize())
throw new IllegalArgumentException("Data offset is out of bounds");
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
ColumnArrayView columnArrayView = render.getVerticalDataView(renderOffsetX + x, renderOffsetZ + z);
SingleFullArrayView fullArrayView = data.get(x, z);
convertColumnData(level, blockX + perRenderWidth * (renderOffsetX+x),
blockZ + perRenderWidth * (renderOffsetZ+z),
columnArrayView, fullArrayView);
if (fullArrayView.doesItExist()) LodUtil.assertTrue(render.doesItExist(renderOffsetX + x, renderOffsetZ + z));
}
}
} else {
final int dataPerRender = 1 << (render.getDataDetail() - data.dataDetail);
final int dataSize = 16 / dataPerRender;
final int vertSize = render.getVerticalSize();
long[] tempRender = new long[dataPerRender * dataPerRender * vertSize];
if (renderOffsetX < 0 || renderOffsetX+dataSize > render.getDataSize() || renderOffsetZ < 0 || renderOffsetZ+dataSize > render.getDataSize())
throw new IllegalArgumentException("Data offset is out of bounds");
for (int x = 0; x < dataSize; x++) {
for (int z = 0; z < dataSize; z++) {
ColumnQuadView tempQuadView = new ColumnQuadView(tempRender, dataPerRender, vertSize, 0, 0, dataPerRender, dataPerRender);
for (int ox = 0; ox < dataPerRender; ox++) {
for (int oz = 0; oz < dataPerRender; oz++) {
ColumnArrayView columnArrayView = tempQuadView.get(ox, oz);
SingleFullArrayView fullArrayView = data.get(x*dataPerRender+ox, z*dataPerRender+oz);
convertColumnData(level, blockX + perRenderWidth * (renderOffsetX+x) + perDataWidth * ox,
blockZ + perRenderWidth * (renderOffsetZ+z) + perDataWidth * oz,
columnArrayView, fullArrayView);
}
}
ColumnArrayView downSampledArrayView = render.getVerticalDataView(renderOffsetX + x, renderOffsetZ + z);
downSampledArrayView.mergeMultiDataFrom(tempQuadView);
}
}
}
}
private static void convertColumnData(IClientLevel level, int blockX, int blockZ, ColumnArrayView columnArrayView, SingleFullArrayView fullArrayView) {
if (!fullArrayView.doesItExist()) return;
// TODO: Set gen mode
int genModeValue = 1;
int dataTotalLength = fullArrayView.getSingleLength();
if (dataTotalLength == 0) return;
if (dataTotalLength > columnArrayView.verticalSize()) {
ColumnArrayView totalColumnData = new ColumnArrayView(new long[dataTotalLength], dataTotalLength, 0, dataTotalLength);
iterateAndConvert(level, blockX, blockZ, genModeValue, totalColumnData, fullArrayView);
columnArrayView.changeVerticalSizeFrom(totalColumnData);
} else {
iterateAndConvert(level, blockX, blockZ, genModeValue, columnArrayView, fullArrayView); //Directly use the arrayView since it fits.
}
}
private static void iterateAndConvert(IClientLevel level, int blockX, int blockZ, int genMode, ColumnArrayView column, SingleFullArrayView data) {
IdBiomeBlockStateMap mapping = data.getMapping();
boolean isVoid = true;
int offset = 0;
for (int i = 0; i < data.getSingleLength(); i++) {
long fullData = data.getSingle(i);
int y = FullFormat.getY(fullData);
int blockLength = FullFormat.getDepth(fullData);
int id = FullFormat.getId(fullData);
int light = FullFormat.getLight(fullData);
IdBiomeBlockStateMap.Entry entry = mapping.get(id);
IBiomeWrapper biome = entry.biome;
IBlockStateWrapper block = entry.blockState;
if (block.equals(AIR)) continue;
isVoid = false;
int color = level.computeBaseColor(new DHBlockPos(blockX, y + level.getMinY(), blockZ), biome, block);
long columnData = ColumnFormat.createDataPoint(y + blockLength, y, color, light, genMode);
column.set(offset, columnData);
offset++;
}
if (isVoid) {
column.set(0, ColumnFormat.createVoidDataPoint((byte) genMode));
}
}
//
//
// /** creates a vertical DataPoint */
// private void writeVerticalData(long[] data, int dataOffset, int maxVerticalData,
// IChunkWrapper chunk, LodBuilderConfig config, int chunkSubPosX, int chunkSubPosZ)
// {
//
// int totalVerticalData = (chunk.getHeight());
// long[] dataToMerge = new long[totalVerticalData];
//
// boolean hasCeiling = MC.getWrappedClientWorld().getDimensionType().hasCeiling();
// boolean hasSkyLight = MC.getWrappedClientWorld().getDimensionType().hasSkyLight();
// byte generation = config.distanceGenerationMode.complexity;
// int count = 0;
// // FIXME: This yAbs is just messy!
// int x = chunk.getMinX() + chunkSubPosX;
// int z = chunk.getMinZ() + chunkSubPosZ;
// int y = chunk.getMaxY(x, z);
//
// boolean topBlock = true;
// if (y < chunk.getMinBuildHeight())
// dataToMerge[0] = DataPointUtil.createVoidDataPoint(generation);
// int maxConnectedLods = Config.Client.Graphics.Quality.verticalQuality.get().maxVerticalData[0];
// while (y >= chunk.getMinBuildHeight()) {
// int height = determineHeightPointFrom(chunk, config, x, y, z);
// // If the lod is at the default height, it must be void data
// if (height < chunk.getMinBuildHeight()) {
// if (topBlock) dataToMerge[0] = DataPointUtil.createVoidDataPoint(generation);
// break;
// }
// y = height - 1;
// // We search light on above air block
// int depth = determineBottomPointFrom(chunk, config, x, y, z,
// count < maxConnectedLods && (!hasCeiling || !topBlock));
// if (hasCeiling && topBlock)
// y = depth;
// int light = getLightValue(chunk, x, y, z, hasCeiling, hasSkyLight, topBlock);
// int color = generateLodColor(chunk, config, x, y, z);
// int lightBlock = light & 0b1111;
// int lightSky = (light >> 4) & 0b1111;
// dataToMerge[count] = DataPointUtil.createDataPoint(height-chunk.getMinBuildHeight(), depth-chunk.getMinBuildHeight(),
// color, lightSky, lightBlock, generation);
// topBlock = false;
// y = depth - 1;
// count++;
// }
// long[] result = DataPointUtil.mergeMultiData(dataToMerge, totalVerticalData, maxVerticalData);
// if (result.length != maxVerticalData) throw new ArrayIndexOutOfBoundsException();
// System.arraycopy(result, 0, data, dataOffset, maxVerticalData);
// }
//
// public static final ELodDirection[] DIRECTIONS = new ELodDirection[] {
// ELodDirection.UP,
// ELodDirection.DOWN,
// ELodDirection.WEST,
// ELodDirection.EAST,
// ELodDirection.NORTH,
// ELodDirection.SOUTH };
//
// private boolean hasCliffFace(IChunkWrapper chunk, int x, int y, int z) {
// for (ELodDirection dir : DIRECTIONS) {
// IBlockDetailWrapper block = chunk.getBlockDetailAtFace(x, y, z, dir);
// if (block == null || !block.hasFaceCullingFor(ELodDirection.OPPOSITE_DIRECTIONS[dir.ordinal()]))
// return true;
// }
// return false;
// }
//
// /**
// * Find the lowest valid point from the bottom.
// * Used when creating a vertical LOD.
// */
// private int determineBottomPointFrom(IChunkWrapper chunk, LodBuilderConfig builderConfig, int xAbs, int yAbs, int zAbs, boolean strictEdge)
// {
// int depth = chunk.getMinBuildHeight();
// IBlockDetailWrapper currentBlockDetail = null;
// if (strictEdge)
// {
// IBlockDetailWrapper blockAbove = chunk.getBlockDetail(xAbs, yAbs + 1, zAbs);
// if (blockAbove != null && Config.Client.WorldGenerator.tintWithAvoidedBlocks.get() && !blockAbove.shouldRender(Config.Client.WorldGenerator.blocksToAvoid.get()))
// { // The above block is skipped. Lets use its skipped color for current block
// currentBlockDetail = blockAbove;
// }
// if (currentBlockDetail == null) currentBlockDetail = chunk.getBlockDetail(xAbs, yAbs, zAbs);
// }
//
// for (int y = yAbs - 1; y >= chunk.getMinBuildHeight(); y--)
// {
// IBlockDetailWrapper nextBlock = chunk.getBlockDetail(xAbs, y, zAbs);
// if (isLayerValidLodPoint(nextBlock)) {
// if (!strictEdge) continue;
// if (currentBlockDetail.equals(nextBlock)) continue;
// if (!hasCliffFace(chunk, xAbs, y, zAbs)) continue;
// }
// depth = (y + 1);
// break;
// }
// return depth;
// }
//
// /** Find the highest valid point from the Top */
// private int determineHeightPointFrom(IChunkWrapper chunk, LodBuilderConfig config, int xAbs, int yAbs, int zAbs)
// {
// //TODO find a way to skip bottom of the world
// int height = chunk.getMinBuildHeight()-1;
// for (int y = yAbs; y >= chunk.getMinBuildHeight(); y--)
// {
// if (isLayerValidLodPoint(chunk, xAbs, y, zAbs))
// {
// height = (y + 1);
// break;
// }
// }
// return height;
// }
//
//
//
// // =====================//
// // constructor helpers //
// // =====================//
//
// /**
// * Generate the color for the given chunk using biome water color, foliage
// * color, and grass color.
// */
// private int generateLodColor(IChunkWrapper chunk, LodBuilderConfig builderConfig, int x, int y, int z)
// {
// int colorInt;
// if (builderConfig.useBiomeColors)
// {
// // I have no idea why I need to bit shift to the right, but
// // if I don't the biomes don't show up correctly.
// colorInt = chunk.getBiome(x, y, z).getColorForBiome(x, z);
// }
// else
// {
// // if we are skipping non-full and non-solid blocks that means we ignore
// // snow, flowers, etc. Get the above block so we can still get the color
// // of the snow, flower, etc. that may be above this block
// colorInt = 0;
// if (chunk.blockPosInsideChunk(x, y+1, z)) {
// IBlockDetailWrapper blockAbove = chunk.getBlockDetail(x, y+1, z);
// if (blockAbove != null && Config.Client.WorldGenerator.tintWithAvoidedBlocks.get() && !blockAbove.shouldRender(Config.Client.WorldGenerator.blocksToAvoid.get()))
// { // The above block is skipped. Lets use its skipped color for current block
// colorInt = blockAbove.getAndResolveFaceColor(null, chunk, new DHBlockPos(x, y+1, z));
// }
// }
//
// // override this block's color if there was a block above this
// // and we were avoiding non-full/non-solid blocks
// if (colorInt == 0) {
// IBlockDetailWrapper detail = chunk.getBlockDetail(x, y, z);
// colorInt = detail.getAndResolveFaceColor(null, chunk, new DHBlockPos(x, y, z));
// }
// }
//
// return colorInt;
// }
//
// /** Gets the light value for the given block position */
// private int getLightValue(IChunkWrapper chunk, int x, int y, int z, boolean hasCeiling, boolean hasSkyLight, boolean topBlock)
// {
// int skyLight;
// int blockLight;
//
// int blockBrightness = chunk.getEmittedBrightness(x, y, z);
// // get the air block above or below this block
// if (hasCeiling && topBlock)
// y--;
// else
// y++;
//
// blockLight = chunk.getBlockLight(x, y, z);
// skyLight = hasSkyLight ? chunk.getSkyLight(x, y, z) : 0;
//
// if (blockLight == -1 || skyLight == -1)
// {
//
// ILevelWrapper world = MC.getWrappedServerWorld();
//
// if (world != null)
// {
// // server world sky light (always accurate)
// blockLight = world.getBlockLight(x, y, z);
//
// if (topBlock && !hasCeiling && hasSkyLight)
// skyLight = DEFAULT_MAX_LIGHT;
// else
// skyLight = hasSkyLight ? world.getSkyLight(x, y, z) : 0;
//
// if (!topBlock && skyLight == 15)
// {
// // we are on predicted terrain, and we don't know what the light here is,
// // lets just take a guess
// skyLight = 12;
// }
// }
// else
// {
// world = MC.getWrappedClientWorld();
// if (world == null)
// {
// blockLight = 0;
// skyLight = 12;
// }
// else
// {
// // client world sky light (almost never accurate)
// blockLight = world.getBlockLight(x, y, z);
// // estimate what the lighting should be
// if (hasSkyLight || !hasCeiling)
// {
// if (topBlock)
// skyLight = DEFAULT_MAX_LIGHT;
// else
// {
// if (hasSkyLight)
// skyLight = world.getSkyLight(x, y, z);
// //else
// // skyLight = 0;
// if (!chunk.isLightCorrect() && (skyLight == 0 || skyLight == 15))
// {
// // we don't know what the light here is,
// // lets just take a guess
// skyLight = 12;
// }
// }
// }
// }
// }
// }
//
// blockLight = LodUtil.clamp(0, Math.max(blockLight, blockBrightness), DEFAULT_MAX_LIGHT);
// return blockLight + (skyLight << 4);
// }
//
// /** Is the block at the given blockPos a valid LOD point? */
// private boolean isLayerValidLodPoint(IBlockDetailWrapper blockDetail)
// {
// EBlocksToAvoid avoid = Config.Client.WorldGenerator.blocksToAvoid.get();
// return blockDetail != null && blockDetail.shouldRender(avoid);
// }
//
// /** Is the block at the given blockPos a valid LOD point? */
// private boolean isLayerValidLodPoint(IChunkWrapper chunk, int x, int y, int z) {
// EBlocksToAvoid avoid = Config.Client.WorldGenerator.blocksToAvoid.get();
// IBlockDetailWrapper block = chunk.getBlockDetail(x, y, z);
// return block != null && block.shouldRender(avoid);
// }
}
@@ -0,0 +1,62 @@
package com.seibel.lod.core.a7.datatype.transform;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.datatype.full.FullFormat;
import com.seibel.lod.core.handlers.dependencyInjection.SingletonInjector;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.lod.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.lod.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.IBiomeWrapper;
import it.unimi.dsi.fastutil.longs.LongArrayList;
public class LodDataBuilder {
private static final IBlockStateWrapper AIR = SingletonInjector.INSTANCE.get(IWrapperFactory.class).getAirBlockStateWrapper();
public static ChunkSizedData createChunkData(IChunkWrapper chunk) {
if (!canGenerateLodFromChunk(chunk)) return null;
ChunkSizedData chunkData = new ChunkSizedData((byte)0, chunk.getChunkPos().x, chunk.getChunkPos().z);
for (int x=0; x<16; x++) {
for (int z=0; z<16; z++) {
LongArrayList longs = new LongArrayList(chunk.getHeight()/4);
int lastY = chunk.getMaxBuildHeight();
IBiomeWrapper biome = chunk.getBiome(x, lastY, z);
IBlockStateWrapper blockState = AIR;
int mappedId = chunkData.getMapping().setAndGetId(biome, blockState);
byte light = (byte) ((chunk.getBlockLight(x,lastY,z) << 4) + chunk.getSkyLight(x,lastY,z));
int y=chunk.getMaxY(x, z);
for (; y>=chunk.getMinBuildHeight(); y--) {
IBiomeWrapper newBiome = chunk.getBiome(x, y, z);
IBlockStateWrapper newBlockState = chunk.getBlockState(x, y, z);
byte newLight = (byte) ((chunk.getBlockLight(x,y,z) << 4) + chunk.getSkyLight(x,y,z));
if (!newBiome.equals(biome) || !newBlockState.equals(blockState)) {
longs.add(FullFormat.encode(mappedId, lastY-y, y+1 - chunk.getMinBuildHeight(), light));
biome = newBiome;
blockState = newBlockState;
mappedId = chunkData.getMapping().setAndGetId(biome, blockState);
light = newLight;
lastY = y;
} else if (newLight != light) {
longs.add(FullFormat.encode(mappedId, lastY-y, y+1 - chunk.getMinBuildHeight(), light));
light = newLight;
lastY = y;
}
}
longs.add(FullFormat.encode(mappedId, lastY-y, y+1 - chunk.getMinBuildHeight(), light));
chunkData.setSingleColumn(longs.toArray(new long[0]), x, z);
}
}
LodUtil.assertTrue(chunkData.emptyCount() == 0);
return chunkData;
}
public static boolean canGenerateLodFromChunk(IChunkWrapper chunk)
{
return chunk != null &&
chunk.isLightCorrect();
}
}
@@ -0,0 +1,213 @@
package com.seibel.lod.core.a7.generation;
import com.seibel.lod.core.a7.datatype.LodDataSource;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.datatype.full.FullDataSource;
import com.seibel.lod.core.a7.pos.DhBlockPos2D;
import com.seibel.lod.core.a7.pos.DhLodPos;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.a7.util.ConcurrentQuadCombinableProviderTree;
import com.seibel.lod.core.a7.util.UncheckedInterruptedException;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.objects.DHChunkPos;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.util.gridList.ArrayGridList;
import org.apache.logging.log4j.Logger;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
public class GenerationQueue implements AutoCloseable {
final ConcurrentQuadCombinableProviderTree<GenerationResult> cqcpTree = new ConcurrentQuadCombinableProviderTree<>();
IGenerator generator = null; //FIXME: This is volatile and need atomic control
private final Logger logger = DhLoggerBuilder.getLogger();
private final ConcurrentHashMap<DhLodPos, CompletableFuture<GenerationResult>> taskMap = new ConcurrentHashMap<>();
private final AtomicReference<ConcurrentHashMap<DhLodPos, CompletableFuture<GenerationResult>>> inProgress = new AtomicReference<>(null);
public GenerationQueue() {}
public void pollAndStartClosest(DhBlockPos2D targetPos) {
if (generator == null) throw new IllegalStateException("generator is null");
if (generator.isBusy()) return;
DhLodPos closest = null;
long closestDist = Long.MAX_VALUE;
int smallestDetail = Integer.MAX_VALUE;
for (DhLodPos key : taskMap.keySet()) {
if (key.detail > smallestDetail) continue;
long dist = key.getCenter().distSquared(targetPos);
if (key.detail == smallestDetail && dist >= closestDist) continue;
closest = key;
closestDist = dist;
smallestDetail = key.detail;
}
if (closest != null) {
CompletableFuture<GenerationResult> future = taskMap.remove(closest);
startFuture(closest, future);
}
}
public void setGenerator(IGenerator generator) {
LodUtil.assertTrue(generator != null);
LodUtil.assertTrue(this.generator == null);
this.generator = generator;
inProgress.set(new ConcurrentHashMap<>(16));
}
public void removeGenerator() {
LodUtil.assertTrue(generator != null);
this.generator = null;
ConcurrentHashMap<DhLodPos, CompletableFuture<GenerationResult>> swapped = this.inProgress.getAndSet(null);
swapped.forEach((k,f) -> f.cancel(true));
}
private CompletableFuture<GenerationResult> createFuture(DhLodPos pos) {
logger.info("Creating gen future for {}", pos);
CompletableFuture<GenerationResult> future = new CompletableFuture<>();
CompletableFuture<GenerationResult> swapped = taskMap.put(pos, future);
LodUtil.assertTrue(swapped == null);
return future;
}
private void startFuture(DhLodPos pos, CompletableFuture<GenerationResult> resultFuture) {
byte dataDetail = generator.getDataDetail();
byte minGenGranularity = generator.getMinGenerationGranularity();
byte maxGenGranularity = generator.getMaxGenerationGranularity();
if (minGenGranularity < 4 || maxGenGranularity < 4) {
throw new IllegalStateException("Generation granularity must be at least 4!");
}
byte minUnitDetail = (byte) (dataDetail + minGenGranularity);
byte maxUnitDetail = (byte) (dataDetail + maxGenGranularity);
LodUtil.assertTrue(pos.detail >= minUnitDetail && pos.detail <= maxUnitDetail);
byte genGranularity = (byte) (pos.detail - dataDetail);
DHChunkPos chunkPosMin = new DHChunkPos(pos.getCorner());
logger.info("Generating section {} with granularity {} at {}", pos, genGranularity, chunkPosMin);
int perCallChunksWidth = 1 << (genGranularity - 4);
CompletableFuture<ArrayGridList<ChunkSizedData>> dataFuture = generator.generate(chunkPosMin, genGranularity);
final ConcurrentHashMap<DhLodPos, CompletableFuture<GenerationResult>> map = this.inProgress.get();
map.put(pos, //FIXME: Slight race condition issue here with map.clear()!
dataFuture.handle((data, ex) -> {
if (ex != null) {
if (ex instanceof CompletionException) {
ex = ex.getCause();
}
UncheckedInterruptedException.rethrowIfIsInterruption(ex);
logger.error("Error generating data for section {}", pos, ex);
throw new CompletionException("Generation failed", ex);
}
LodUtil.assertTrue(data != null);
if (data.gridSize < (1 << (genGranularity-4))) {
logger.error(
"Generator at {} returned {} by {} chunks but requested granularity was {}, which expect at least {} by {} chunks! ",
pos, data.gridSize, data.gridSize, genGranularity, perCallChunksWidth, perCallChunksWidth);
throw new RuntimeException("Generation failed. Generator returned less data than requested!");
}
logger.info("Completed generating {} by {} chunks to sections that overlaps {}",
data.gridSize, data.gridSize, pos);
return data;
}).thenApply((list) -> {
GenerationResult result = new GenerationResult();
result.dataList.addAll(list);
return result;
}).handle((r, e) -> {
if (e!=null) resultFuture.completeExceptionally(e); else resultFuture.complete(r);
map.remove(pos);
return null;
})
);
}
public CompletableFuture<LodDataSource> generate(DhSectionPos sectionPos) {
byte maxGen = (byte) (generator.getMaxGenerationGranularity() + generator.getDataDetail());
if (sectionPos.sectionDetail > maxGen) {
int count = 1 << (sectionPos.sectionDetail - maxGen);
DhLodPos minPos = sectionPos.getCorner(maxGen);
ArrayList<CompletableFuture<GenerationResult>> futures = new ArrayList<>(count*count);
for (int x = 0; x < count; x++) {
for (int z = 0; z < count; z++) {
DhLodPos subPos = new DhLodPos(maxGen, minPos.x + x, minPos.z + z);
futures.add(cqcpTree.createOrUseExisting(subPos, this::createFuture));
}
}
// FIXME: Does `allOf` have correct behaviour when one of the futures fails?
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply((v) -> {
FullDataSource newSource = FullDataSource.createEmpty(sectionPos);
for (CompletableFuture<GenerationResult> future : futures) {
try {
GenerationResult result = future.join();
for (ChunkSizedData data : result.dataList) {
if (data.getBBoxLodPos().overlaps(sectionPos.getSectionBBoxPos())) newSource.update(data);
}
} catch (Exception e) {
UncheckedInterruptedException.rethrowIfIsInterruption(e);
// else log
logger.error("Error generating data for section {}", sectionPos, e);
}
}
return newSource;
});
} else {
DhLodPos lodPos = sectionPos.getSectionBBoxPos();
return cqcpTree.createOrUseExisting(lodPos, this::createFuture).thenApply(
(result) -> {
if (result == null || result.dataList.isEmpty()) return FullDataSource.createEmpty(sectionPos);
FullDataSource newSource = FullDataSource.createEmpty(sectionPos);
for (ChunkSizedData data : result.dataList) {
if (data.getBBoxLodPos().overlaps(sectionPos.getSectionBBoxPos())) newSource.update(data);
}
return newSource;
});
}
}
@Override
public void close() {
//TODO
}
//
// DhBlockPos2D lastPlayerPos = new DhBlockPos2D(0, 0);
// final ConcurrentHashMap<DhSectionPos, WeakReference<PlaceHolderRenderSource>> trackers = new ConcurrentHashMap<>();
// final BiConsumer<DhSectionPos, ChunkSizedData> writeConsumer;
// final HashSet<Request, CompletableFuture<?>> inProgressSections = new HashSet<>();
//
// public void track(PlaceHolderRenderSource source) {
// //logger.info("Tracking source {} at {}", source, source.getSectionPos());
// trackers.put(source.getSectionPos(), new WeakReference<>(source));
// }
//
// private void update() {
// LinkedList<DhSectionPos> toRemove = new LinkedList<>();
// for (DhSectionPos pos : trackers.keySet()) {
// WeakReference<PlaceHolderRenderSource> ref = trackers.get(pos);
// if (ref.get() == null) {
// toRemove.add(pos);
// }
// }
// for (DhSectionPos pos : toRemove) {
// trackers.remove(pos);
// }
// }
// //FIXME: Do optimizations on polling closest to player. (Currently its a O(n) search!)
// //FIXME: Do not return sections that is already being generated.
// //FIXME: Optimize the checks for inProgressSections.
// private DhSectionPos pollClosest(DhBlockPos2D playerPos) {
// update();
// DhSectionPos closest = null;
// long closestDist = Long.MAX_VALUE;
// for (DhSectionPos pos : trackers.keySet()) {
// if (inProgressSections.contains(pos)) {
// continue;
// }
// long distSqr = pos.getCenter().getCenter().distSquared(playerPos);
// if (distSqr < closestDist) {
// closest = pos;
// closestDist = distSqr;
// }
// }
// if (closest != null) inProgressSections.add(closest);
// return closest;
// }
}
@@ -0,0 +1,19 @@
package com.seibel.lod.core.a7.generation;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.util.CombinableResult;
import java.util.ArrayList;
public class GenerationResult implements CombinableResult<GenerationResult> {
public final ArrayList<ChunkSizedData> dataList = new ArrayList<>();
@Override
public GenerationResult combineWith(GenerationResult b, GenerationResult c, GenerationResult d) {
dataList.ensureCapacity(dataList.size() + b.dataList.size() + c.dataList.size() + d.dataList.size());
dataList.addAll(b.dataList);
dataList.addAll(c.dataList);
dataList.addAll(d.dataList);
return this;
}
}
@@ -0,0 +1,23 @@
package com.seibel.lod.core.a7.generation;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.datatype.transform.LodDataBuilder;
import com.seibel.lod.core.objects.DHChunkPos;
import com.seibel.lod.core.util.gridList.ArrayGridList;
import com.seibel.lod.core.wrapperInterfaces.chunk.IChunkWrapper;
import java.util.concurrent.CompletableFuture;
public interface IChunkGenerator extends IGenerator {
CompletableFuture<ArrayGridList<IChunkWrapper>> generateChunks(DHChunkPos chunkPosMin, byte granularity);
@Override
default CompletableFuture<ArrayGridList<ChunkSizedData>> generate(DHChunkPos chunkPosMin, byte granularity) {
return generateChunks(chunkPosMin, granularity).thenApply(chunks -> {
ArrayGridList<ChunkSizedData> chunkData = new ArrayGridList<>(chunks.gridSize);
chunks.forEachPos((x, y) -> chunkData.set(x, y, LodDataBuilder.createChunkData(chunks.get(x, y))));
return chunkData;
});
}
}
@@ -0,0 +1,31 @@
package com.seibel.lod.core.a7.generation;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.objects.DHChunkPos;
import com.seibel.lod.core.util.gridList.ArrayGridList;
import java.util.concurrent.CompletableFuture;
public interface IGenerator extends AutoCloseable {
// What is the detail / resolution of the data? (This will offset the generation granularity)
// (minimum detail is 0, maximum detail is 255) (though that high isn't really... realistic)
// (0 = 1x1 block per data, 1 = 2x2 block per data, 2 = 4x4 block per data... etc.)
// TODO: System currently only supports 1x1 block per data.
byte getDataDetail();
// What is the min batch size of a single generation?
// (minimum return value is 4 since that's the MC chunk size)
// (4 -> 16x16 data per call, 5 -> 32x32 data per call, 6 -> 64x64 data per call... etc.)
byte getMinGenerationGranularity();
// What is the max batch size of a single generation?
// (minimum return value is 4 since that's the MC chunk size)
// (4 -> 16x16 data per call, 5 -> 32x32 data per call, 6 -> 64x64 data per call... etc.)
byte getMaxGenerationGranularity();
// Start a generation event
// (Note that the chunkPos is always aligned to the granularity)
// (For example, if the granularity is 4, data detail is 0, the chunkPos will be aligned to 16x16 blocks)
CompletableFuture<ArrayGridList<ChunkSizedData>> generate(DHChunkPos chunkPosMin, byte granularity);
// Return whether the generator is currently busy and cannot accept new generation requests.
boolean isBusy();
}
@@ -0,0 +1,107 @@
package com.seibel.lod.core.a7.level;
import com.seibel.lod.core.a7.render.LodQuadTree;
import com.seibel.lod.core.a7.util.FileScanner;
import com.seibel.lod.core.a7.save.io.file.RemoteDataFileHandler;
import com.seibel.lod.core.a7.save.io.render.RenderFileHandler;
import com.seibel.lod.core.a7.pos.DhBlockPos2D;
import com.seibel.lod.core.a7.render.RenderBufferHandler;
import com.seibel.lod.core.a7.save.structure.ClientOnlySaveStructure;
import com.seibel.lod.core.config.Config;
import com.seibel.lod.core.handlers.dependencyInjection.SingletonInjector;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.objects.DHBlockPos;
import com.seibel.lod.core.objects.math.Mat4f;
import com.seibel.lod.core.a7.render.a7LodRenderer;
import com.seibel.lod.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.lod.core.wrapperInterfaces.minecraft.IProfilerWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
import org.apache.logging.log4j.Logger;
import java.util.concurrent.CompletableFuture;
public class DhClientLevel implements IClientLevel {
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
public final ClientOnlySaveStructure save;
public final RemoteDataFileHandler dataFileHandler;
public final RenderFileHandler renderFileHandler;
public final RenderBufferHandler renderBufferHandler; //TODO: Should this be owned by renderer?
public final IClientLevelWrapper level;
public a7LodRenderer renderer = null;
public LodQuadTree tree;
public DhClientLevel(ClientOnlySaveStructure save, IClientLevelWrapper level) {
this.save = save;
save.getDataFolder(level).mkdirs();
save.getRenderCacheFolder(level).mkdirs();
dataFileHandler = new RemoteDataFileHandler(this, save.getDataFolder(level));
renderFileHandler = new RenderFileHandler(dataFileHandler, this, save.getRenderCacheFolder(level));
tree = new LodQuadTree(this, Config.Client.Graphics.Quality.lodChunkRenderDistance.get()*16,
MC_CLIENT.getPlayerBlockPos().x, MC_CLIENT.getPlayerBlockPos().z, renderFileHandler);
renderBufferHandler = new RenderBufferHandler(tree);
this.level = level;
FileScanner.scanFile(save, level, dataFileHandler, renderFileHandler);
LOGGER.info("Started DHLevel for {} with saves at {}", level, save);
}
@Override
public void dumpRamUsage() {
//TODO
}
@Override
public void clientTick() {
tree.tick(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos()));
renderBufferHandler.update();
}
@Override
public void render(Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks, IProfilerWrapper profiler) {
if (renderer == null) {
renderer = new a7LodRenderer(this);
}
renderer.drawLODs(mcModelViewMatrix, mcProjectionMatrix, partialTicks, profiler);
}
@Override
public RenderBufferHandler getRenderBufferHandler() {
return renderBufferHandler;
}
@Override
public int computeBaseColor(DHBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper block) {
return 0; //TODO
}
@Override
public IClientLevelWrapper getClientLevelWrapper() {
return level;
}
@Override
public ILevelWrapper getLevelWrapper()
{
return this.level;
}
@Override
public int getMinY() {
return level.getMinHeight();
}
@Override
public CompletableFuture<Void> save() {
return renderFileHandler.flushAndSave();
}
@Override
public void close() {
renderFileHandler.close();
LOGGER.info("Closed DHLevel for {}", level);
}
}
@@ -0,0 +1,184 @@
package com.seibel.lod.core.a7.level;
import com.seibel.lod.core.a7.generation.GenerationQueue;
import com.seibel.lod.core.a7.generation.IGenerator;
import com.seibel.lod.core.a7.render.LodQuadTree;
import com.seibel.lod.core.a7.save.io.file.GeneratedDataFileHandler;
import com.seibel.lod.core.a7.util.FileScanner;
import com.seibel.lod.core.a7.save.io.file.DataFileHandler;
import com.seibel.lod.core.a7.save.io.render.RenderFileHandler;
import com.seibel.lod.core.a7.pos.DhBlockPos2D;
import com.seibel.lod.core.a7.render.RenderBufferHandler;
import com.seibel.lod.core.a7.save.structure.LocalSaveStructure;
import com.seibel.lod.core.builders.worldGeneration.BatchGenerator;
import com.seibel.lod.core.config.Config;
import com.seibel.lod.core.handlers.dependencyInjection.SingletonInjector;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.objects.DHBlockPos;
import com.seibel.lod.core.objects.math.Mat4f;
import com.seibel.lod.core.a7.render.a7LodRenderer;
import com.seibel.lod.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.lod.core.wrapperInterfaces.minecraft.IProfilerWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.IServerLevelWrapper;
import org.apache.logging.log4j.Logger;
import java.util.concurrent.CompletableFuture;
public class DhClientServerLevel implements IClientLevel, IServerLevel {
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
public final LocalSaveStructure save;
public final DataFileHandler dataFileHandler;
public GenerationQueue generationQueue = null;
public RenderFileHandler renderFileHandler = null;
public RenderBufferHandler renderBufferHandler = null; //TODO: Should this be owned by renderer?
public final IServerLevelWrapper serverLevel;
public IClientLevelWrapper clientLevel;
public a7LodRenderer renderer = null;
public LodQuadTree tree = null;
public BatchGenerator worldGenerator = null;
public DhClientServerLevel(LocalSaveStructure save, IServerLevelWrapper level) {
this.serverLevel = level;
this.save = save;
save.getDataFolder(level).mkdirs();
save.getRenderCacheFolder(level).mkdirs();
generationQueue = new GenerationQueue();
dataFileHandler = new GeneratedDataFileHandler(this, save.getDataFolder(level), generationQueue);
FileScanner.scanFile(save, serverLevel, dataFileHandler, null);
LOGGER.info("Started DHLevel for {} with saves at {}", level, save);
}
@Override
public void clientTick() {
//LOGGER.info("Client tick for {}", level);
if (tree != null) tree.tick(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos()));
if (renderBufferHandler != null) renderBufferHandler.update();
}
@Override
public void serverTick() {
//TODO Update network packet and stuff or state or etc..
}
public void startRenderer(IClientLevelWrapper clientLevel) {
LOGGER.info("Starting renderer for {}", this);
if (renderBufferHandler != null || this.clientLevel != null) {
LOGGER.warn("Tried to call startRenderer() on {} when renderer is already setup!", this);
return;
}
this.clientLevel = clientLevel;
// TODO: Make a registry for generators for modding support.
worldGenerator = new BatchGenerator(this);
generationQueue.setGenerator(worldGenerator);
renderFileHandler = new RenderFileHandler(dataFileHandler, this, save.getRenderCacheFolder(serverLevel));
tree = new LodQuadTree(this, Config.Client.Graphics.Quality.lodChunkRenderDistance.get()*16,
MC_CLIENT.getPlayerBlockPos().x, MC_CLIENT.getPlayerBlockPos().z, renderFileHandler);
renderBufferHandler = new RenderBufferHandler(tree);
FileScanner.scanFile(save, serverLevel, null, renderFileHandler);
}
@Override
public void render(Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks, IProfilerWrapper profiler) {
if (renderBufferHandler == null) {
LOGGER.error("Tried to call render() on {} when renderer has not been started!", this);
return;
}
if (renderer == null) {
renderer = new a7LodRenderer(this);
}
renderer.drawLODs(mcModelViewMatrix, mcProjectionMatrix, partialTicks, profiler);
}
public void stopRenderer() {
LOGGER.info("Stopping renderer for {}", this);
if (renderBufferHandler == null) {
LOGGER.warn("Tried to call stopRenderer() on {} when renderer is already closed!", this);
return;
}
tree.close();
tree = null;
generationQueue.removeGenerator();
try {
worldGenerator.close();
} catch (Exception e) {
LOGGER.error("Error closing world generator", e);
}
worldGenerator = null;
renderBufferHandler.close();
renderBufferHandler = null;
renderFileHandler.flushAndSave(); //Ignore the completion feature so that this action is async
renderFileHandler.close();
renderFileHandler = null;
}
@Override
public RenderBufferHandler getRenderBufferHandler() {
return renderBufferHandler;
}
@Override
public int computeBaseColor(DHBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper block) {
return clientLevel.computeBaseColor(pos, biome, block);
}
@Override
public IClientLevelWrapper getClientLevelWrapper() {
return clientLevel;
}
@Override
public ILevelWrapper getLevelWrapper()
{
return this.serverLevel;
}
@Override
public void dumpRamUsage() {
//TODO
}
@Override
public int getMinY() {
return serverLevel.getMinHeight();
}
@Override
public CompletableFuture<Void> save() {
if (renderFileHandler != null) {
return renderFileHandler.flushAndSave().thenCompose(v -> dataFileHandler.flushAndSave());
} else {
return dataFileHandler.flushAndSave();
}
}
@Override
public void close() {
if (worldGenerator != null) worldGenerator.close();
if (renderer != null) renderer.close();
if (tree != null) tree.close();
if (renderBufferHandler != null) renderBufferHandler.close();
if (renderFileHandler != null) renderFileHandler.close();
dataFileHandler.close();
LOGGER.info("Closed {}", this);
}
@Override
public void doWorldGen() {
if (worldGenerator != null) {
worldGenerator.update();
if (generationQueue != null)
generationQueue.pollAndStartClosest(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos()));
}
}
@Override
public IServerLevelWrapper getServerLevelWrapper() {
return serverLevel;
}
}
@@ -0,0 +1,69 @@
package com.seibel.lod.core.a7.level;
import com.seibel.lod.core.a7.util.FileScanner;
import com.seibel.lod.core.a7.save.io.file.DataFileHandler;
import com.seibel.lod.core.a7.save.structure.LocalSaveStructure;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.IServerLevelWrapper;
import org.apache.logging.log4j.Logger;
import java.util.concurrent.CompletableFuture;
public class DhServerLevel implements IServerLevel
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public final LocalSaveStructure save;
public final DataFileHandler dataFileHandler;
public final IServerLevelWrapper level;
public DhServerLevel(LocalSaveStructure save, IServerLevelWrapper level) {
this.save = save;
this.level = level;
save.getDataFolder(level).mkdirs();
dataFileHandler = new DataFileHandler(this, save.getDataFolder(level), null); //FIXME: GenerationQueue
FileScanner.scanFile(save, level, dataFileHandler, null);
LOGGER.info("Started DHLevel for {} with saves at {}", level, save);
}
public void serverTick() {
//Nothing for now
}
@Override
public int getMinY() {
return level.getMinHeight();
}
@Override
public void dumpRamUsage() {
//TODO
}
@Override
public void close() {
dataFileHandler.close();
LOGGER.info("Closed DHLevel for {}", level);
}
@Override
public CompletableFuture<Void> save() {
return dataFileHandler.flushAndSave();
}
@Override
public void doWorldGen() {
// FIXME: No world gen for server side only for now
}
@Override
public IServerLevelWrapper getServerLevelWrapper() {
return level;
}
@Override
public ILevelWrapper getLevelWrapper()
{
return this.level;
}
}
@@ -0,0 +1,21 @@
package com.seibel.lod.core.a7.level;
import com.seibel.lod.core.a7.render.RenderBufferHandler;
import com.seibel.lod.core.objects.DHBlockPos;
import com.seibel.lod.core.objects.math.Mat4f;
import com.seibel.lod.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.lod.core.wrapperInterfaces.minecraft.IProfilerWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.IClientLevelWrapper;
public interface IClientLevel extends ILevel {
void clientTick();
void render(Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks, IProfilerWrapper profiler);
RenderBufferHandler getRenderBufferHandler();
int computeBaseColor(DHBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper block);
IClientLevelWrapper getClientLevelWrapper();
}
@@ -0,0 +1,17 @@
package com.seibel.lod.core.a7.level;
import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
import java.util.concurrent.CompletableFuture;
public interface ILevel extends AutoCloseable
{
int getMinY();
CompletableFuture<Void> save();
void dumpRamUsage();
/** May return either a client or server level wrapper. */
ILevelWrapper getLevelWrapper();
}
@@ -0,0 +1,10 @@
package com.seibel.lod.core.a7.level;
import com.seibel.lod.core.wrapperInterfaces.world.IServerLevelWrapper;
public interface IServerLevel extends ILevel {
void serverTick();
void doWorldGen();
IServerLevelWrapper getServerLevelWrapper();
}
@@ -0,0 +1,61 @@
package com.seibel.lod.core.a7.pos;
import com.seibel.lod.core.objects.DHBlockPos;
import com.seibel.lod.core.objects.Pos2D;
import com.seibel.lod.core.util.LodUtil;
import java.util.Objects;
public class DhBlockPos2D {
public final int x;
public final int z;
public DhBlockPos2D(int x, int z) {
this.x = x;
this.z = z;
}
public DhBlockPos2D(DHBlockPos blockPos) {
this.x = blockPos.x;
this.z = blockPos.z;
}
public DhBlockPos2D add(DhBlockPos2D other) {
return new DhBlockPos2D(x + other.x, z + other.z);
}
public DhBlockPos2D subtract(DhBlockPos2D other) {
return new DhBlockPos2D(x - other.x, z - other.z);
}
public double dist(DhBlockPos2D other) {
return Math.sqrt(Math.pow(x - other.x, 2) + Math.pow(z - other.z, 2));
}
public long distSquared(DhBlockPos2D other) {
return LodUtil.pow2((long)x - other.x) + LodUtil.pow2((long)z - other.z);
}
public Pos2D toPos2D() {
return new Pos2D(x, z);
}
public static DhBlockPos2D fromPos2D(Pos2D pos) {
return new DhBlockPos2D(pos.x, pos.y);
}
@Override
public String toString() {
return "(" + x + ", " + z + ")";
}
@Override
public boolean equals(Object obj) {
if (obj instanceof DhBlockPos2D) {
DhBlockPos2D other = (DhBlockPos2D)obj;
return x == other.x && z == other.z;
}
return false;
}
@Override
public int hashCode() {
return Integer.hashCode(x) ^ Integer.hashCode(z);
}
}
@@ -0,0 +1,102 @@
package com.seibel.lod.core.a7.pos;
import com.seibel.lod.core.util.LodUtil;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
public class DhLodPos implements Comparable<DhLodPos> {
public final byte detail;
public final int x;
public final int z;
public DhLodPos(byte detail, int x, int z) {
this.detail = detail;
this.x = x;
this.z = z;
}
public String toString() {
return "[" + detail + "*" + x + "," + z + "]";
}
public DhLodUnit getX() {
return new DhLodUnit(detail, x);
}
public DhLodUnit getZ() {
return new DhLodUnit(detail, z);
}
public int getWidth() {
return 1 << detail;
}
public int getWidth(byte detail) {
LodUtil.assertTrue(detail <= this.detail);
return 1 << (this.detail - detail);
}
public static int blockWidth(byte detail) {
return 1 << detail;
}
public DhBlockPos2D getCenter() {
return new DhBlockPos2D(getX().toBlock() + (getWidth() >> 1), getZ().toBlock() + (getWidth() >> 1));
}
public DhBlockPos2D getCorner() {
return new DhBlockPos2D(getX().toBlock(), getZ().toBlock());
}
public DhLodPos getCorner(byte newDetail) {
LodUtil.assertTrue(newDetail <= detail);
return new DhLodPos(newDetail, x << (detail-newDetail), z << (detail-newDetail));
}
public DhLodPos convertUpwardsTo(byte newDetail) {
LodUtil.assertTrue(newDetail >= detail);
return new DhLodPos(newDetail, Math.floorDiv(x, 1<<(newDetail-detail)), Math.floorDiv(z, 1<<(newDetail-detail)));
}
public DhLodPos getChild(int child0to3) {
if (child0to3 < 0 || child0to3 > 3) throw new IllegalArgumentException("child0to3 must be between 0 and 3");
if (detail <= 0) throw new IllegalStateException("detail must be greater than 0");
return new DhLodPos((byte) (detail - 1),
x * 2 + (child0to3 & 1),
z * 2 + ((child0to3 & 2) >> 1));
}
public int getChildIndexOfParent() {
return (x & 1) + ((z & 1) << 1);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DhLodPos dhLodPos = (DhLodPos) o;
return detail == dhLodPos.detail && x == dhLodPos.x && z == dhLodPos.z;
}
@Override
public int hashCode() {
return Objects.hash(detail, x, z);
}
public boolean overlaps(DhLodPos other) {
if (equals(other)) return true;
if (detail == other.detail) return false;
if (detail > other.detail) {
return this.equals(other.convertUpwardsTo(this.detail));
} else {
return other.equals(this.convertUpwardsTo(other.detail));
}
}
public DhLodPos add(DhLodUnit width) {
if (width.detail < detail) throw new IllegalArgumentException("add called with width.detail < pos detail");
return new DhLodPos(detail, x + width.convertTo(detail).value, z + width.convertTo(detail).value);
}
@Override
public int compareTo(@NotNull DhLodPos o) {
return detail != o.detail ? Integer.compare(detail, o.detail) : x != o.x ? Integer.compare(x, o.x) : Integer.compare(z, o.z);
}
}
@@ -0,0 +1,29 @@
package com.seibel.lod.core.a7.pos;
public class DhLodUnit {
public final byte detail;
public final int value;
public DhLodUnit(byte detail, int value) {
this.detail = detail;
this.value = value;
}
public int toBlock() {
return value << detail;
}
public static DhLodUnit fromBlock(int block, byte targetDetail) {
return new DhLodUnit(targetDetail, Math.floorDiv(block, 1<<targetDetail));
}
public DhLodUnit convertTo(byte targetDetail) {
if (detail == targetDetail) {
return this;
}
if (detail > targetDetail) { //TODO check if this is correct
return new DhLodUnit(targetDetail, value << (detail - targetDetail));
}
return new DhLodUnit(targetDetail, Math.floorDiv(value, 1<<(targetDetail-detail)));
}
}
@@ -0,0 +1,112 @@
package com.seibel.lod.core.a7.pos;
import com.seibel.lod.core.enums.ELodDirection;
import com.seibel.lod.core.util.LodUtil;
import java.util.function.Consumer;
public class DhSectionPos {
public final byte sectionDetail;
public final int sectionX; // in sectionDetail level grid
public final int sectionZ; // in sectionDetail level grid
public DhSectionPos(byte sectionDetail, int sectionX, int sectionZ) {
this.sectionDetail = sectionDetail;
this.sectionX = sectionX;
this.sectionZ = sectionZ;
}
public DhLodPos getCenter(byte returnDetailLevel) {
LodUtil.assertTrue(returnDetailLevel <= sectionDetail, "returnDetailLevel must be less than sectionDetail");
if (returnDetailLevel == sectionDetail)
return new DhLodPos(sectionDetail, sectionX, sectionZ);
byte offset = (byte) (sectionDetail - returnDetailLevel);
return new DhLodPos(returnDetailLevel, (sectionX << offset)+(1 << (offset -1)),
(sectionZ << offset)+(1 << (offset -1)));
}
public DhLodPos getCorner(byte returnDetailLevel) {
LodUtil.assertTrue(returnDetailLevel <= sectionDetail, "returnDetailLevel must be less than sectionDetail");
byte offset = (byte) (sectionDetail - returnDetailLevel);
return new DhLodPos(returnDetailLevel, sectionX << offset, sectionZ << offset);
}
public DhLodUnit getWidth(byte returnDetailLevel) {
LodUtil.assertTrue(returnDetailLevel <= sectionDetail, "returnDetailLevel must be less than sectionDetail");
byte offset = (byte) (sectionDetail - returnDetailLevel);
return new DhLodUnit(sectionDetail, 1 << offset);
}
public DhLodPos getCenter() {
return getCenter((byte)0);
}
public DhLodPos getCorner() {
return getCorner((byte) (sectionDetail-1));
}
public DhLodUnit getWidth() {
return getWidth(sectionDetail);
}
public DhSectionPos getChild(int child0to3){
if (child0to3 < 0 || child0to3 > 3) throw new IllegalArgumentException("child0to3 must be between 0 and 3");
if (sectionDetail <= 0) throw new IllegalStateException("section detail must be greater than 0");
return new DhSectionPos((byte) (sectionDetail - 1),
sectionX * 2 + (child0to3 & 1),
sectionZ * 2 + ((child0to3 & 2) >> 1));
}
public int getChildIndexOfParent() {
return (sectionX & 1) + ((sectionZ & 1) << 1);
}
public void forEachChild(Consumer<DhSectionPos> callback){
for (int i = 0; i < 4; i++) {
callback.accept(getChild(i));
}
}
public DhSectionPos getParent(){
return new DhSectionPos((byte) (sectionDetail + 1), sectionX >> 1, sectionZ >> 1);
}
public DhSectionPos getAdjacent(ELodDirection dir) {
return new DhSectionPos(sectionDetail, sectionX + dir.getNormal().x, sectionZ + dir.getNormal().z);
}
public DhLodPos getSectionBBoxPos() {
return new DhLodPos(sectionDetail, sectionX, sectionZ);
}
/**
* NOTE: This does not consider yOffset!
*/
public boolean overlaps(DhSectionPos other){
return getSectionBBoxPos().overlaps(other.getSectionBBoxPos());
}
@Override
public String toString() {
return "{" + sectionDetail +
"*" + sectionX +
"," + sectionZ +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DhSectionPos that = (DhSectionPos) o;
return sectionDetail == that.sectionDetail &&
sectionX == that.sectionX &&
sectionZ == that.sectionZ;
}
@Override
public int hashCode() {
return Integer.hashCode(sectionDetail) ^
Integer.hashCode(sectionX) ^
Integer.hashCode(sectionZ);
}
// Serialize() is different from toString() as this requires it to NEVER be changed, and should be in a short format
public String serialize() {
return "[" + sectionDetail + ',' + sectionX + ',' + sectionZ + ']';
}
}
@@ -0,0 +1,468 @@
package com.seibel.lod.core.a7.render;
import com.seibel.lod.core.a7.datatype.LodRenderSource;
import com.seibel.lod.core.a7.datatype.column.ColumnRenderSource;
import com.seibel.lod.core.a7.level.IClientLevel;
import com.seibel.lod.core.a7.pos.DhBlockPos2D;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.a7.save.io.render.IRenderSourceProvider;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.objects.Pos2D;
import com.seibel.lod.core.util.DetailDistanceUtil;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.util.gridList.MovableGridRingList;
import org.apache.logging.log4j.Logger;
// QuadTree built from several layers of 2d ring buffers
/**
* This quadTree structure is the core of the DH mod.
* This class represent a circular quadTree of lodSection
*
* Each section at level n is populated in one (sometimes more than one) ways:
* -by constructing it from the data of all the children sections (lower levels)
* -by loading from file
* -by adding data with the lodBuilder
*/
public class LodQuadTree implements AutoCloseable {
/**
* Note: all config value should be via the class that extends this class, and
* by implementing different abstract methods
*/
private static final byte LAYER_BEGINNING_OFFSET = ColumnRenderSource.SECTION_SIZE_OFFSET;
private static final boolean SUPER_VERBOSE_LOGGING = false;
public final byte getLayerDataDetailOffset(byte sectionDetail) {
return ColumnRenderSource.SECTION_SIZE_OFFSET;
}
public final byte getLayerSectionDetailOffset(byte dataDetail) {
return ColumnRenderSource.SECTION_SIZE_OFFSET;
}
public final byte getLayerDataDetail(byte sectionDetail) {
return (byte) (sectionDetail - getLayerDataDetailOffset(sectionDetail));
}
public final byte getLayerSectionDetail(byte dataDetail) {
return (byte) (dataDetail + getLayerSectionDetailOffset(dataDetail));
}
private static final Logger LOGGER = DhLoggerBuilder.getLogger("LodQuadTree");
public final byte numbersOfSectionLevels;
private final MovableGridRingList<LodRenderSection>[] ringLists;
public final int viewDistance;
private final IRenderSourceProvider renderSourceProvider;
private final IClientLevel level; //FIXME: Proper hierarchy to remove this reference!
/**
* Constructor of the quadTree
* @param viewDistance View distance in blocks
* @param initialPlayerX player x coordinate
* @param initialPlayerZ player z coordinate
*/
public LodQuadTree(IClientLevel level, int viewDistance, int initialPlayerX, int initialPlayerZ, IRenderSourceProvider provider) {
DetailDistanceUtil.updateSettings(); //TODO: Move this to somewhere else
this.level = level;
renderSourceProvider = provider;
this.viewDistance = viewDistance;
{ // Calculate the max section detail
byte maxDataDetailLevel = getMaxDetailInRange(viewDistance * Math.sqrt(2));
byte topSectionLevel = getLayerSectionDetail(maxDataDetailLevel);
numbersOfSectionLevels = (byte) (topSectionLevel + 1);
ringLists = new MovableGridRingList[numbersOfSectionLevels - LAYER_BEGINNING_OFFSET];
}
{ // Construct the ringLists
LOGGER.info("Creating ringLists with player center at {}", new Pos2D(initialPlayerX, initialPlayerZ));
for (byte i = LAYER_BEGINNING_OFFSET; i < numbersOfSectionLevels; i++) {
byte targetDataDetail = getLayerDataDetail(i);
int maxDist = getFurthestDistance(targetDataDetail);
int halfSize = LodUtil.ceilDiv(maxDist, (1 << i)) + 2; // +2 to make sure the section is fully contained in the ringList
{
DhSectionPos checkerPos = new DhSectionPos(i, halfSize, halfSize);
byte checkedDetail = calculateExpectedDetailLevel(new DhBlockPos2D(initialPlayerX, initialPlayerZ),checkerPos);
LodUtil.assertTrue(checkedDetail > targetDataDetail,
"in {}, getFuthestDistance return {} which would be contained in range {}, but calculateExpectedDetailLevel at {} is {} <= {}",
i, maxDist, halfSize - 2, checkerPos, checkedDetail, targetDataDetail);
}
LOGGER.info("ringlist centered in {} with halfSize {} (maxDist {}, dataDetail {})", new Pos2D(initialPlayerX >> i, initialPlayerZ >> i), halfSize, maxDist, targetDataDetail);
ringLists[i - LAYER_BEGINNING_OFFSET] = new MovableGridRingList<>(halfSize,
initialPlayerX >> i, initialPlayerZ >> i);
LOGGER.info("Creating ringList {}: {}", i, ringLists[i - LAYER_BEGINNING_OFFSET].toString());
}
}
}
/**
* This method return the LodSection given the Section Pos
* @param pos the section positon.
* @return the LodSection
*/
public LodRenderSection getSection(DhSectionPos pos) {
return getSection(pos.sectionDetail, pos.sectionX, pos.sectionZ);
}
/**
* This method returns the RingList of a given detail level
* @apiNote The returned ringList should not be modified!
* @param detailLevel the detail level
* @return the RingList
*/
public MovableGridRingList<LodRenderSection> getRingList(byte detailLevel) {
return ringLists[detailLevel - LAYER_BEGINNING_OFFSET];
}
/**
* This method returns the number of detail levels in the quadTree
* @return the number of detail levels
*/
public byte getNumbersOfSectionLevels() {
return numbersOfSectionLevels;
}
public byte getStartingSectionLevel() {
return LAYER_BEGINNING_OFFSET;
}
/**
* This method return the LodSection at the given detail level and level coordinate x and z
* @param detailLevel detail level of the section
* @param x x coordinate of the section
* @param z z coordinate of the section
* @return the LodSection
*/
public LodRenderSection getSection(byte detailLevel, int x, int z) {
return ringLists[detailLevel - LAYER_BEGINNING_OFFSET].get(x, z);
}
/**
* This method will compute the detail level based on player position and section pos
* Override this method if you want to use a different algorithm
* @param playerPos player position as a reference for calculating the detail level
* @param sectionPos section position
* @return detail level of this section pos
*/
public byte calculateExpectedDetailLevel(DhBlockPos2D playerPos, DhSectionPos sectionPos) {
return DetailDistanceUtil.getDetailLevelFromDistance(
playerPos.dist(sectionPos.getCenter().getCenter()));
}
/**
* The method will return the highest detail level in a circle around the center
* Override this method if you want to use a different algorithm
* Note: the returned distance should always be the ceiling estimation of the distance
* //TODO: Make this input a bbox or a circle or something....
* @param distance the circle radius
* @return the highest detail level in the circle
*/
public byte getMaxDetailInRange(double distance) {
return DetailDistanceUtil.getDetailLevelFromDistance(distance);
}
/**
* The method will return the furthest distance to the center for the given detail level
* Override this method if you want to use a different algorithm
* Note: the returned distance should always be the ceiling estimation of the distance
* //TODO: Make this return a bbox instead of a distance in circle
* @param detailLevel detail level
* @return the furthest distance to the center, in blocks
*/
public int getFurthestDistance(byte detailLevel) {
return (int)Math.ceil(DetailDistanceUtil.getDrawDistanceFromDetail(detailLevel + 1));
// +1 because that's the border to the next detail level, and we want to include up to it.
}
/**
* Given a section pos at level n this method returns the parent section at level n+1
* @param pos the section positon
* @return the parent LodSection
*/
public LodRenderSection getParentSection(DhSectionPos pos) {
return getSection(pos.getParent());
}
/**
* Given a section pos at level n and a child index this method return the
* child section at level n-1
* @param pos
* @param child0to3 since there are 4 possible children this index identify which one we are getting
* @return one of the child LodSection
*/
public LodRenderSection getChildSection(DhSectionPos pos, int child0to3) {
return getSection(pos.getChild(child0to3));
}
private LodRenderSection _set(MovableGridRingList<LodRenderSection> list, int x, int z, LodRenderSection t) {
LodUtil.assertTrue(t != null, "setting null at [{},{}] in {}", x, z, list.toString());
LodUtil.assertTrue(t.pos.sectionX == x && t.pos.sectionZ == z, "pos {} != [{},{}] in {}", t.pos, x, z, list.toString());
LodRenderSection s = list.setChained(x,z,t);
LodUtil.assertTrue(s != null, "returned null at [{},{}]: {}", x, z, list.toString());
LodUtil.assertTrue(s == t,"{} != {} in {}",s,t, list.toString());
return s;
}
private LodRenderSection _getNotNull(MovableGridRingList<LodRenderSection> list, int x, int z) {
LodUtil.assertTrue(list.inRange(x,z), "[{},{}] not in range of {}", x, z, list.toString());
LodRenderSection s = list.get(x,z);
LodUtil.assertTrue(s != null, "getting null at [{},{}] in {}", x, z, list.toString());
LodUtil.assertTrue(s.pos.sectionX == x && s.pos.sectionZ == z, "obj {} != [{},{}] in {}", s, x, z, list.toString());
return s;
}
private LodRenderSection _get(MovableGridRingList<LodRenderSection> list, int x, int z) {
LodRenderSection s = list.get(x,z);
LodUtil.assertTrue(s == null || (s.pos.sectionX == x && s.pos.sectionZ == z), "obj {} != [{},{}] in {}", s, x, z, list.toString());
return s;
}
/**
* This function update the quadTree based on the playerPos and the current game configs (static and global)
* @param playerPos the reference position for the player
*/
public void tick(DhBlockPos2D playerPos) {
for (int sectLevel = LAYER_BEGINNING_OFFSET; sectLevel < numbersOfSectionLevels; sectLevel++) {
if (!ringLists[sectLevel - LAYER_BEGINNING_OFFSET].getCenter().equals(
new Pos2D(playerPos.x >> sectLevel, playerPos.z >> sectLevel))) {
LOGGER.info("TreeTick: Moving ring list {} from {} to {}", sectLevel,
ringLists[sectLevel - LAYER_BEGINNING_OFFSET].getCenter(),
new Pos2D(playerPos.x >> sectLevel, playerPos.z >> sectLevel));
ringLists[sectLevel - LAYER_BEGINNING_OFFSET]
.move(playerPos.x >> sectLevel, playerPos.z >> sectLevel,
LodRenderSection::dispose);
}
}
// First tick pass: update all sections' childCount from bottom level to top level. Step:
// If sectLevel is bottom && section != null:
// - set childCount to 0
// If section != null && child != 0: //TODO: Should I move this createChild steps to Second tick pass?
// - // Section will be in the unloaded state.
// - create parent if not at final level and if it doesn't exist, with childCount = 1
// - for each child:
// - if null, create new with childCount = 0 (force load due to neighboring issues)
// - else if childCount == -1, set childCount = 0 (rescue it)
// - set childCount to 4
// Else:
// - Calculate targetLevel at that section
// - If sectLevel == numberOfSectionLevels - 1:
// - // Section is the top level.
// - If targetLevel > dataLevel@sectLevel && section != null:
// - set childCount to -1 (Signal that section is to be freed) (this prob not be rescued as it is the top level)
// - If targetLevel <= dataLevel@sectLevel && section == null: (direct use the current sectLevel's dataLevel)
// - create new section with childCount = 0
// - Else:
// - // Section is not the top level. So we also need to consider the parent.
// - If targetLevel >= dataLevel@(sectLevel+1) && section != null: (use the next level's dataLevel)
// - Parent's childCount-- (Assert parent != null && childCount > 0 before decrementing)
// - // Note that this doesn't necessarily mean this section will be freed as it may be rescued later
// due to neighboring quadrants not able to be freed (they pass targetLevel checks or has children)
// or due to parent's layer is in the Always Cascade mode. (containerType == null)
// - set childCount to -1 (Signal that this section will be freed if not rescued)
// - If targetLevel < dataLevel@(sectLevel+1) && section == null: (use the next level's dataLevel)
// - create new section with childCount = 0
// - Parent's childCount++ (Create parent if needed)
for (byte sectLevel = LAYER_BEGINNING_OFFSET; sectLevel < numbersOfSectionLevels; sectLevel++) {
final MovableGridRingList<LodRenderSection> ringList = ringLists[sectLevel - LAYER_BEGINNING_OFFSET];
final MovableGridRingList<LodRenderSection> childRingList =
sectLevel == LAYER_BEGINNING_OFFSET ? null : ringLists[sectLevel - LAYER_BEGINNING_OFFSET - 1];
final MovableGridRingList<LodRenderSection> parentRingList =
sectLevel == numbersOfSectionLevels - 1 ? null : ringLists[sectLevel - LAYER_BEGINNING_OFFSET + 1];
final byte f_sectLevel = sectLevel;
ringList.forEachPosOrdered((section, pos) -> {
if (f_sectLevel == LAYER_BEGINNING_OFFSET && section != null) {
section.childCount = 0;
//LOGGER.info("sect {} in first layer with non-null. Reset childCount", section.pos);
}
if (section != null && section.childCount != 0) {
// Section will be in the unloaded state.
if (SUPER_VERBOSE_LOGGING) LOGGER.info("sect {} has child", section.pos);
if (parentRingList != null) {
LodRenderSection parent = _get(parentRingList, pos.x >> 1, pos.y >> 1);
if (parent == null) {
if (SUPER_VERBOSE_LOGGING) LOGGER.info("sect {} missing parent. Creating at {}", section.pos, section.pos.getParent());
parent = _set(parentRingList, pos.x >> 1, pos.y >> 1, new LodRenderSection(section.pos.getParent()));
parent.childCount++;
if (SUPER_VERBOSE_LOGGING) LOGGER.info("parent sect {} now has {} childs.", section.pos.getParent(), parent.childCount);
}
LodUtil.assertTrue(parent.childCount <= 4 && parent.childCount > 0);
}
for (byte i = 0; i < 4; i++) {
DhSectionPos childPos = section.pos.getChild(i);
LodUtil.assertTrue(childRingList != null);
LodRenderSection child = _get(childRingList, childPos.sectionX, childPos.sectionZ);
if (child == null) {
if (SUPER_VERBOSE_LOGGING) LOGGER.info("sect {} missing child at {}. Creating.", section.pos, childPos);
child = _set(childRingList, childPos.sectionX, childPos.sectionZ, new LodRenderSection(childPos));
child.childCount = 0;
} else if (child.childCount == -1) {
if (SUPER_VERBOSE_LOGGING) LOGGER.info("sect {} rescued child at {}.", section.pos, childPos);
child.childCount = 0;
}
}
section.childCount = 4;
} else {
final DhSectionPos sectPos = section != null ? section.pos : new DhSectionPos(f_sectLevel, pos.x, pos.y);
LodUtil.assertTrue(sectPos.sectionDetail == f_sectLevel
&& sectPos.sectionX == pos.x && sectPos.sectionZ == pos.y,
"sectPos {} != {} @ {}", sectPos, pos, f_sectLevel);
byte targetLevel = calculateExpectedDetailLevel(playerPos, sectPos);
if (SUPER_VERBOSE_LOGGING) LOGGER.info("0 child sect {}(null?{}) - target:{}/{} (parent:{})", sectPos, section == null,
targetLevel, getLayerDataDetail(f_sectLevel),
f_sectLevel == numbersOfSectionLevels-1 ? "N/A" : getLayerDataDetail((byte) (f_sectLevel+1)));
if (f_sectLevel == numbersOfSectionLevels -1) {
// Section is in the top level.
if (targetLevel > getLayerDataDetail(f_sectLevel) && section != null) {
if (SUPER_VERBOSE_LOGGING) LOGGER.info("sect {} in top & target>current. Mark as free.", sectPos);
section.childCount = -1;
}
if (targetLevel <= getLayerDataDetail(f_sectLevel) && section == null) {
if (SUPER_VERBOSE_LOGGING) LOGGER.info("null sect {} in top & target<=current. Creating.", sectPos);
section = _set(ringList, pos.x, pos.y, new LodRenderSection(sectPos));
}
} else {
// Section is not the top level. So we also need to consider the parent.
if (targetLevel >= getLayerDataDetail((byte) (f_sectLevel+1)) && section != null) {
if (SUPER_VERBOSE_LOGGING) LOGGER.info("sect {} target>=nextLevel. Mark as free.", sectPos);
LodUtil.assertTrue(parentRingList != null);
LodRenderSection parent = _getNotNull(parentRingList, pos.x >> 1, pos.y >> 1);
LodUtil.assertTrue(parent.childCount <= 4 && parent.childCount > 0);
parent.childCount--;
if (SUPER_VERBOSE_LOGGING) LOGGER.info("parent sect {} now has {} child.", sectPos, parent.childCount);
section.childCount = -1;
}
if (targetLevel < getLayerDataDetail((byte) (f_sectLevel+1)) && section == null) {
if (SUPER_VERBOSE_LOGGING) LOGGER.info("null sect {} target<nextLevel. Creating.", sectPos);
section = _set(ringList, pos.x, pos.y, new LodRenderSection(sectPos));
LodUtil.assertTrue(parentRingList != null);
LodRenderSection parent = _get(parentRingList, pos.x >> 1, pos.y >> 1);
if (parent == null) {
if (SUPER_VERBOSE_LOGGING) LOGGER.info("sect {} missing parent. Creating at {}", sectPos, sectPos.getParent());
parent = _set(parentRingList, pos.x >> 1, pos.y >> 1, new LodRenderSection(sectPos.getParent()));
}
parent.childCount++;
if (SUPER_VERBOSE_LOGGING) LOGGER.info("parent sect {} now has {} childs.", sectPos.getParent(), parent.childCount);
}
}
}
// Final quick assert to insure section pos is correct.
if (section != null) {
LodUtil.assertTrue(section.pos.sectionDetail == f_sectLevel, "section.pos: " + section.pos + " vs level: " + f_sectLevel);
LodUtil.assertTrue(section.pos.sectionX == pos.x, "section.pos: " + section.pos + " vs pos: " + pos);
LodUtil.assertTrue(section.pos.sectionZ == pos.y, "section.pos: " + section.pos + " vs pos: " + pos);
}
});
}
// Second tick pass:
// Cascade the layers that is in Always Cascade Mode from top to bottom. (Not yet exposed or used)
// At the same time, load and unload sections (and can also be used to assert everything is working). Step:
// ===Assertion steps===
// assert childCount == 4 || childCount == 0 || childCount == -1
// if childCount == 4 assert all children exist
// if childCount == 0 assert all children are null
// if childCount == -1 assert parent childCount is 0
// // ======================
// if childCount == 4 && section is loaded:
// - unload section
// if childCount == 0 && section is unloaded:
// - load section
// if childCount == -1: // (section can be loaded or unloaded, due to fast movement)
// - set this section to null (TODO: Is this needed to be first or last or don't matter for concurrency?)
// - If loaded unload section
for (byte sectLevel = (byte) (numbersOfSectionLevels - 1); sectLevel >= LAYER_BEGINNING_OFFSET; sectLevel--) {
final MovableGridRingList<LodRenderSection> ringList = ringLists[sectLevel - LAYER_BEGINNING_OFFSET];
final MovableGridRingList<LodRenderSection> childRingList =
sectLevel == LAYER_BEGINNING_OFFSET ? null : ringLists[sectLevel - LAYER_BEGINNING_OFFSET - 1];
final boolean doCascade = false; // TODO: Utilize this cascade mode or at least expose this option
ringList.forEachPosOrdered((section, pos) -> {
if (section == null) return;
// Cascade layers
// if (doCascade && section.childCount == 0) {
// LodUtil.assertTrue(childRingList != null);
// // Create childs to cascade the layer.
// for (byte i = 0; i < 4; i++) {
// DhSectionPos childPos = section.pos.getChild(i);
// LodRenderSection child = childRingList.get(childPos.sectionX, childPos.sectionZ);
// if (child == null) {
// child = childRingList.setChained(childPos.sectionX, childPos.sectionZ,
// new LodRenderSection(childPos));
// child.childCount = 0;
// } else {
// LodUtil.assertTrue(child.childCount == -1,
// "Self has child count 0 but an existing child's child count != -1!");
// child.childCount = 0;
// }
// }
// section.childCount = 4;
// }
// Call load on new sections, and tick on existing ones, and dispose old sections
if (section.childCount == -1) {
if (section.pos.sectionDetail < numbersOfSectionLevels-1)
LodUtil.assertTrue(getParentSection(section.pos).childCount == 0);
ringList.set(pos.x, pos.y, null);
section.dispose();
return;
} else {
if (!section.isLoaded() && !section.isLoading()) {
section.load(renderSourceProvider);
} else if (section.isOutdated()) {
section.reload(renderSourceProvider);
}
if (section.childCount == 4) section.disableRender();
if (section.childCount == 0) section.enableRender(level, this);
section.tick(this, level);
}
// Assertion steps
LodUtil.assertTrue(section.childCount == 4 || section.childCount == 0);
if (section.pos.sectionDetail == LAYER_BEGINNING_OFFSET) LodUtil.assertTrue(section.childCount == 0);
if (section.pos.sectionDetail != LAYER_BEGINNING_OFFSET) {
LodRenderSection child0 = getChildSection(section.pos, 0);
LodRenderSection child1 = getChildSection(section.pos, 1);
LodRenderSection child2 = getChildSection(section.pos, 2);
LodRenderSection child3 = getChildSection(section.pos, 3);
if (section.childCount == 4) LodUtil.assertTrue(
child0 != null && child0.childCount != -1 &&
child1 != null && child1.childCount != -1 &&
child2 != null && child2.childCount != -1 &&
child3 != null && child3.childCount != -1,
"Sect {} child count 4 but child has null or is being disposed: {} {} {} {}",
section.pos, child0, child1, child2, child3);
if (section.childCount == 0) LodUtil.assertTrue(
(child0 == null || child0.childCount == -1) &&
(child1 == null || child1.childCount == -1) &&
(child2 == null || child2.childCount == -1) &&
(child3 == null || child3.childCount == -1),
"Sect {} child count 0 but child is neither null or being disposed: {} {} {} {}",
section.pos, child0, child1, child2, child3);
}
});
}
}
public String getDebugString() {
StringBuilder sb = new StringBuilder();
for (byte i = 0; i < ringLists.length; i++) {
sb.append("Layer ").append(i + LAYER_BEGINNING_OFFSET).append(":\n");
sb.append(ringLists[i].toDetailString());
sb.append("\n");
sb.append("\n");
}
return sb.toString();
}
@Override
public void close() {
for (MovableGridRingList<LodRenderSection> ringList : ringLists) {
ringList.forEach((section) -> {
if (section != null) section.dispose();
});
}
}
}
@@ -0,0 +1,119 @@
package com.seibel.lod.core.a7.render;
import com.seibel.lod.core.a7.level.IClientLevel;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.a7.datatype.LodRenderSource;
import com.seibel.lod.core.a7.save.io.render.IRenderSourceProvider;
import java.util.concurrent.CompletableFuture;
public class LodRenderSection {
public final DhSectionPos pos;
/* Following used for LodQuadTree tick() method, and ONLY for that method! */
// the number of children of this section
// (Should always be 4 after tick() is done, or 0 only if this is an unloaded node)
public byte childCount = 0;
// TODO: Should I provide a way to change the render source?
private LodRenderSource lodRenderSource;
private CompletableFuture<LodRenderSource> loadFuture;
private boolean isRenderEnabled = false;
private IRenderSourceProvider provider = null;
// Create sub region
public LodRenderSection(DhSectionPos pos) {
this.pos = pos;
}
public void enableRender(IClientLevel level, LodQuadTree quadTree) {
if (isRenderEnabled) return;
loadFuture = provider.read(pos);
isRenderEnabled = true;
}
public void disableRender() {
if (!isRenderEnabled) return;
if (lodRenderSource != null) {
lodRenderSource.disableRender();
lodRenderSource.dispose();
lodRenderSource = null;
}
if (loadFuture != null) {
loadFuture.cancel(true);
loadFuture = null;
}
isRenderEnabled = false;
}
public void load(IRenderSourceProvider renderDataProvider) {
provider = renderDataProvider;
}
public void reload(IRenderSourceProvider renderDataProvider) {
if (loadFuture != null) {
loadFuture.cancel(true);
loadFuture = null;
}
if (lodRenderSource != null) {
lodRenderSource.dispose();
lodRenderSource = null;
}
loadFuture = renderDataProvider.read(pos);
}
public void tick(LodQuadTree quadTree, IClientLevel level) {
if (loadFuture != null && loadFuture.isDone()) {
lodRenderSource = loadFuture.join();
loadFuture = null;
if (isRenderEnabled) {
lodRenderSource.enableRender(level, quadTree);
}
}
if (lodRenderSource != null) {
lodRenderSource.flushWrites(level);
}
}
public void dispose() {
if (lodRenderSource != null) {
lodRenderSource.dispose();
} else if (loadFuture != null) {
loadFuture.cancel(true);
}
}
public boolean canRender() {
return isLoaded() && isRenderEnabled && lodRenderSource != null && lodRenderSource.isRenderReady();
}
public boolean isLoaded() {
return provider != null;
}
//FIXME: Used by RenderBufferHandler
public int FIXME_BYPASS_DONT_USE_getChildCount() {
return childCount;
}
public boolean isLoading() {
return false;
}
public boolean isOutdated() {
return lodRenderSource != null && !lodRenderSource.isValid();
}
public LodRenderSource getRenderContainer() {
return lodRenderSource;
}
public String toString() {
return "LodRenderSection{" +
"pos=" + pos +
", childCount=" + childCount +
", lodRenderSource=" + lodRenderSource +
", loadFuture=" + loadFuture +
", isRenderEnabled=" + isRenderEnabled +
'}';
}
}
@@ -0,0 +1,61 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.a7.render;
import com.seibel.lod.core.render.LodRenderProgram;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.util.StatsMap;
public abstract class RenderBuffer implements AutoCloseable
{
// ======================================================================
// ====================== Methods for implementations ===================
// ======================================================================
// ========== Called by render thread ==========
/* Called on... well... rendering.
* Return false if nothing rendered. (Optional) */
public abstract boolean render(a7LodRenderer renderContext);
// ========== Called by any thread. (thread safe) ==========
/* Called by anyone. This method is allowed to throw exceptions, but
* are never allowed to modify any values. This should behave the same
* to other methods as if the method have never been called.
* Note: This method is PURELY for debug or stats logging ONLY! */
public abstract void debugDumpStats(StatsMap statsMap);
// ========= Called only when 1 thread is using it =======
/* This method is called when object is no longer in use.
* Called either after uploadBuffers() returned false (On buffer Upload
* thread), or by others when the object is not being used. (not in build,
* upload, or render state). */
public abstract void close();
public static final int DEFAULT_MEMORY_ALLOCATION = (LodUtil.LOD_VERTEX_FORMAT.getByteSize() * 3) * 8;
public static final int QUADS_BYTE_SIZE = LodUtil.LOD_VERTEX_FORMAT.getByteSize() * 4;
public static final int MAX_QUADS_PER_BUFFER = (1024 * 1024 * 1) / QUADS_BYTE_SIZE;
public static final int FULL_SIZED_BUFFER = MAX_QUADS_PER_BUFFER * QUADS_BYTE_SIZE;
}
@@ -0,0 +1,218 @@
package com.seibel.lod.core.a7.render;
import com.seibel.lod.core.a7.datatype.LodRenderSource;
import com.seibel.lod.core.handlers.dependencyInjection.SingletonInjector;
import com.seibel.lod.core.objects.DHBlockPos;
import com.seibel.lod.core.objects.Pos2D;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.util.gridList.MovableGridRingList;
import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import java.util.Comparator;
import java.util.concurrent.atomic.AtomicReference;
public class RenderBufferHandler {
public final LodQuadTree target;
private final MovableGridRingList<RenderBufferNode> renderBufferNodes;
class RenderBufferNode implements AutoCloseable {
public final DhSectionPos pos;
public volatile RenderBufferNode[] children = null;
public final AtomicReference<RenderBuffer> renderBufferSlotOpaque = new AtomicReference<>();
public final AtomicReference<RenderBuffer> renderBufferSlotTransparent = new AtomicReference<>();
public RenderBufferNode(DhSectionPos pos) {
this.pos = pos;
}
/**
* This will render all opaque lods
* @param renderContext
*/
public void renderOpaque(a7LodRenderer renderContext) {
RenderBuffer buff;
buff = renderBufferSlotOpaque.get();
if (buff != null) {
buff.render(renderContext);
} else {
RenderBufferNode[] childs = children;
if (childs != null) {
for (RenderBufferNode child : childs) {
child.renderOpaque(renderContext);
}
}
}
}
/**
* This will render all transparent lods
* @param renderContext
*/
public void renderTransparent(a7LodRenderer renderContext) {
RenderBuffer buff;
buff = renderBufferSlotTransparent.get();
if (buff != null) {
buff.render(renderContext);
} else {
RenderBufferNode[] childs = children;
if (childs != null) {
for (RenderBufferNode child : childs) {
child.renderTransparent(renderContext);
}
}
}
}
//TODO: In the future make this logic a bit more complex so that when children are just created,
// the buffer is only unloaded if all children's buffers are ready. This will make the
// transition between buffers no longer causing any flicker.
public void update() {
LodRenderSection section = target.getSection(pos);
// If this fails, there may be concurrent modification of the quad tree
// (as this update() should be called from the same thread that calls update() on the quad tree)
LodUtil.assertTrue(section != null);
LodRenderSource container = section.getRenderContainer();
// Update self's render buffer state
boolean shouldRender = section.canRender();
if (!shouldRender) {
//TODO: Does this really need to force the old buffer to not be rendered?
// RenderBuffer buff = renderBufferSlot.getAndSet(null);
// if (buff != null) {
// buff.close();
// }
} else {
LodUtil.assertTrue(container != null); // section.isLoaded() should have ensured this
container.trySwapRenderBuffer(target, renderBufferSlotOpaque, renderBufferSlotTransparent);
}
// Update children's render buffer state
// TODO: Improve this! (Checking section.isLoaded() as if its not loaded, it can only be because
// it has children. (But this logic is... really hard to read!)
// FIXME: Above comment is COMPLETELY WRONG! I am an idiot!
boolean shouldHaveChildren = section.FIXME_BYPASS_DONT_USE_getChildCount() > 0;
if (shouldHaveChildren) {
if (children == null) {
RenderBufferNode[] childs = new RenderBufferNode[4];
for (int i = 0; i < 4; i++) {
childs[i] = new RenderBufferNode(pos.getChild(i));
}
children = childs;
}
for (RenderBufferNode child : children) {
child.update();
}
} else {
if (children != null) {
//FIXME: Concurrency issue here: If render thread is concurrently using the child's buffer,
// and this thread got priority to close the buffer, it causes a bug wher the render thread
// will be using a closed buffer!!!!
RenderBufferNode[] childs = children;
children = null;
for (RenderBufferNode child : childs) {
child.close();
}
}
}
}
@Override
public void close() {
if (children != null) {
for (RenderBufferNode child : children) {
child.close();
}
}
RenderBuffer buff;
buff = renderBufferSlotOpaque.getAndSet(null);
if (buff != null) {
buff.close();
}
buff = renderBufferSlotTransparent.getAndSet(null);
if (buff != null) {
buff.close();
}
}
}
public RenderBufferHandler(LodQuadTree target) {
this.target = target;
MovableGridRingList<LodRenderSection> referenceList = target.getRingList((byte) (target.getNumbersOfSectionLevels() - 1));
Pos2D center = referenceList.getCenter();
renderBufferNodes = new MovableGridRingList<>(referenceList.getHalfSize(), center);
}
public void render(a7LodRenderer renderContext) {
//TODO: This might get locked by update() causing move() call. Is there a way to avoid this?
// Maybe dupe the base list and use atomic swap on render? Or is this not worth it?
//TODO: Directional culling
//TODO: Ordered by distance
renderBufferNodes.forEachOrdered(n -> n.renderOpaque(renderContext));
if(a7LodRenderer.transparencyEnabled)
renderBufferNodes.forEachOrdered(n -> n.renderTransparent(renderContext));
}
public void update() {
byte topDetail = (byte) (target.getNumbersOfSectionLevels() - 1);
MovableGridRingList<LodRenderSection> referenceList = target.getRingList(topDetail);
Pos2D center = referenceList.getCenter();
//boolean moved = renderBufferNodes.getCenter().x != center.x || renderBufferNodes.getCenter().y != center.y;
renderBufferNodes.move(center.x, center.y, RenderBufferNode::close); // Note: may lock the list
renderBufferNodes.forEachPosOrdered((node, pos) -> {
DhSectionPos sectPos = new DhSectionPos(topDetail, pos.x, pos.y);
LodRenderSection section = target.getSection(sectPos);
if (section == null) {
// If section is null, but node exists, remove node
if (node != null) {
renderBufferNodes.remove(pos).close();
}
// If section is null, continue
return;
}
// If section is not null, but node does not exist, create node
if (node == null) {
node = renderBufferNodes.setChained(pos, new RenderBufferNode(sectPos));
}
// Node should be not null here
// Update node
node.update();
});
/**TODO improve the ordering*/
/* DHBlockPos playerPos = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class).getPlayerBlockPos();
int x = playerPos.x;
int z = playerPos.z;
Comparator<RenderBufferNode> byDistance = new Comparator<RenderBufferNode>() {
@Override
public int compare(RenderBufferNode o1, RenderBufferNode o2) {
if ((o1 == null) && (o2 == null)) {
return 0;
} else if (o1 == null) {
return 1;
} else if (o2 == null) {
return -1;
}
int x1 = o1.pos.sectionX;
int z1 = o1.pos.sectionZ;
int x2 = o2.pos.sectionX;
int z2 = o2.pos.sectionZ;
return Integer.compare((x1 - x) ^ 2 + (z1 - z) ^ 2, (x2 - x) ^ 2 + (z2 - z) ^ 2);
}
};
renderBufferNodes.sort(byDistance);*/
}
public void close() {
renderBufferNodes.clear(RenderBufferNode::close);
}
}
@@ -0,0 +1,333 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.a7.render;
import com.seibel.lod.core.a7.level.IClientLevel;
import com.seibel.lod.core.config.Config;
import com.seibel.lod.core.config.types.ConfigEntry;
import com.seibel.lod.core.enums.rendering.EDebugMode;
import com.seibel.lod.core.enums.rendering.EFogColorMode;
import com.seibel.lod.core.handlers.dependencyInjection.SingletonInjector;
import com.seibel.lod.core.logging.ConfigBasedLogger;
import com.seibel.lod.core.logging.ConfigBasedSpamLogger;
import com.seibel.lod.core.objects.DHBlockPos;
import com.seibel.lod.core.objects.math.Mat4f;
import com.seibel.lod.core.objects.math.Vec3d;
import com.seibel.lod.core.objects.math.Vec3f;
import com.seibel.lod.core.render.GLProxy;
import com.seibel.lod.core.render.LodFogConfig;
import com.seibel.lod.core.render.LodRenderProgram;
import com.seibel.lod.core.render.RenderUtil;
import com.seibel.lod.core.render.objects.GLState;
import com.seibel.lod.core.render.objects.GLVertexBuffer;
import com.seibel.lod.core.render.objects.QuadElementBuffer;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.wrapperInterfaces.config.ILodConfigWrapperSingleton;
import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import com.seibel.lod.core.wrapperInterfaces.minecraft.IProfilerWrapper;
import com.seibel.lod.core.wrapperInterfaces.misc.ILightMapWrapper;
import org.apache.logging.log4j.LogManager;
import org.lwjgl.opengl.GL32;
import java.awt.*;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
/**
* This is where all the magic happens. <br>
* This is where LODs are draw to the world.
*
* @author James Seibel
* @version 2022-8-21
*/
public class a7LodRenderer
{
public static final ConfigBasedLogger EVENT_LOGGER = new ConfigBasedLogger(LogManager.getLogger(a7LodRenderer.class),
() -> Config.Client.Advanced.Debugging.DebugSwitch.logRendererBufferEvent.get());
public static ConfigBasedSpamLogger tickLogger = new ConfigBasedSpamLogger(LogManager.getLogger(a7LodRenderer.class),
() -> Config.Client.Advanced.Debugging.DebugSwitch.logRendererBufferEvent.get(),1);
public static final boolean ENABLE_DRAW_LAG_SPIKE_LOGGING = false;
public static final boolean ENABLE_DUMP_GL_STATE = true;
public static final long DRAW_LAG_SPIKE_THRESHOLD_NS = TimeUnit.NANOSECONDS.convert(20, TimeUnit.MILLISECONDS);
public static final boolean ENABLE_IBO = true;
public static boolean transparencyEnabled = true;
public static boolean fakeOceanFloor = true;
public void setupOffset(DHBlockPos pos) {
Vec3d cam = MC_RENDER.getCameraExactPosition();
shaderProgram.setModelPos(new Vec3f((float) (pos.x - cam.x), (float) (pos.y - cam.y), (float) (pos.z - cam.z)));
}
public void drawVbo(GLVertexBuffer vbo) {
vbo.bind();
shaderProgram.bindVertexBuffer(vbo.getId());
GL32.glDrawElements(GL32.GL_TRIANGLES, (vbo.getVertexCount()/4)*6,
quadIBO.getType(), 0);
}
public static class LagSpikeCatcher {
long timer = System.nanoTime();
public LagSpikeCatcher() {}
public void end(String source) {
if (!ENABLE_DRAW_LAG_SPIKE_LOGGING) return;
timer = System.nanoTime() - timer;
if (timer> DRAW_LAG_SPIKE_THRESHOLD_NS) { //4 ms
EVENT_LOGGER.debug("NOTE: "+source+" took "+Duration.ofNanos(timer)+"!");
}
}
}
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
public EDebugMode previousDebugMode = null;
public final IClientLevel level;
// The shader program
LodRenderProgram shaderProgram = null;
public QuadElementBuffer quadIBO = null;
public boolean isSetupComplete = false;
public a7LodRenderer(IClientLevel level)
{
this.level = level;
}
private boolean closeCalled = false;
public void close() {
if (closeCalled) {
EVENT_LOGGER.warn("close() called twice!");
return;
}
closeCalled = true;
GLProxy.getInstance().recordOpenGlCall(this::cleanup);
}
public void drawLODs(Mat4f baseModelViewMatrix, Mat4f baseProjectionMatrix, float partialTicks, IProfilerWrapper profiler)
{
if (closeCalled) {
EVENT_LOGGER.error("drawLODs() called after close()!");
return;
}
// get MC's shader program
// Save all MC render state
LagSpikeCatcher drawSaveGLState = new LagSpikeCatcher();
GLState currentState = new GLState();
if (ENABLE_DUMP_GL_STATE) {
tickLogger.debug("Saving GL state: {}", currentState);
}
drawSaveGLState.end("drawSaveGLState");
GLProxy glProxy = GLProxy.getInstance();
if (Config.Client.Graphics.FogQuality.disableVanillaFog.get())
MC_RENDER.tryDisableVanillaFog();
// The Buffer manager
RenderBufferHandler bufferHandler = level.getRenderBufferHandler();
//===================//
// draw params setup //
//===================//
profiler.push("LOD draw setup");
/*---------Set GL State--------*/
// Make sure to unbind current VBO so we don't mess up vanilla settings
//GL32.glBindFramebuffer(GL32.GL_FRAMEBUFFER, MC_RENDER.getTargetFrameBuffer());
GL32.glViewport(0,0, MC_RENDER.getTargetFrameBufferViewportWidth(), MC_RENDER.getTargetFrameBufferViewportHeight());
GL32.glBindBuffer(GL32.GL_ARRAY_BUFFER, 0);
// set the required open GL settings
ConfigEntry<EDebugMode> debugModeConfig = Config.Client.Advanced.Debugging.debugMode;
if (debugModeConfig.get() == EDebugMode.SHOW_DETAIL_WIREFRAME
|| debugModeConfig.get() == EDebugMode.SHOW_GENMODE_WIREFRAME
|| debugModeConfig.get() == EDebugMode.SHOW_WIREFRAME
|| debugModeConfig.get() == EDebugMode.SHOW_OVERLAPPING_QUADS_WIREFRAME) {
GL32.glPolygonMode(GL32.GL_FRONT_AND_BACK, GL32.GL_LINE);
//GL32.glDisable(GL32.GL_CULL_FACE);
}
else {
GL32.glPolygonMode(GL32.GL_FRONT_AND_BACK, GL32.GL_FILL);
GL32.glEnable(GL32.GL_CULL_FACE);
}
GL32.glEnable(GL32.GL_DEPTH_TEST);
// GL32.glDisable(GL32.GL_DEPTH_TEST);
GL32.glDepthFunc(GL32.GL_LESS);
transparencyEnabled = Config.Client.Graphics.Quality.transparency.get().tranparencyEnabled;
fakeOceanFloor = Config.Client.Graphics.Quality.transparency.get().fakeTransparencyEnabled;
if(transparencyEnabled) {
GL32.glBlendFunc(GL32.GL_SRC_ALPHA, GL32.GL_ONE_MINUS_SRC_ALPHA);
GL32.glEnable(GL32.GL_BLEND);
}else{
GL32.glDisable(GL32.GL_BLEND);
}
GL32.glClear(GL32.GL_DEPTH_BUFFER_BIT);
/*---------Bind required objects--------*/
// Setup LodRenderProgram and the LightmapTexture if it has not yet been done
// also binds LightmapTexture, VAO, and ShaderProgram
if (!isSetupComplete) {
setup();
} else {
LodFogConfig newConfig = shaderProgram.isShaderUsable();
if (newConfig != null) {
shaderProgram.free();
shaderProgram = new LodRenderProgram(newConfig);
}
shaderProgram.bind();
}
GL32.glActiveTexture(GL32.GL_TEXTURE0);
//LightmapTexture lightmapTexture = new LightmapTexture();
/*---------Get required data--------*/
int vanillaBlockRenderedDistance = MC_RENDER.getRenderDistance() * LodUtil.CHUNK_WIDTH;
Mat4f modelViewProjectionMatrix = RenderUtil.createCombinedModelViewProjectionMatrix(baseProjectionMatrix, baseModelViewMatrix, partialTicks);
/*---------Fill uniform data--------*/
// Fill the uniform data. Note: GL33.GL_TEXTURE0 == texture bindpoint 0
shaderProgram.fillUniformData(modelViewProjectionMatrix,
MC_RENDER.isFogStateSpecial() ? getSpecialFogColor(partialTicks) : getFogColor(partialTicks),
0, MC.getWrappedClientWorld().getHeight(), MC.getWrappedClientWorld().getMinHeight(), RenderUtil.getFarClipPlaneDistanceInBlocks(),
vanillaBlockRenderedDistance, MC_RENDER.isFogStateSpecial());
// Note: Since lightmapTexture is changing every frame, it's faster to recreate it than to reuse the old one.
ILightMapWrapper lightmap = MC_RENDER.getLightmapWrapper();
lightmap.bind();
if (ENABLE_IBO) quadIBO.bind();
//lightmapTexture.fillData(MC_RENDER.getLightmapTextureWidth(), MC_RENDER.getLightmapTextureHeight(), MC_RENDER.getLightmapPixels());
//GL32.glEnable( GL32.GL_POLYGON_OFFSET_FILL );
//GL32.glPolygonOffset( 1f, 1f );
//===========//
// rendering //
//===========//
profiler.popPush("LOD draw");
LagSpikeCatcher draw = new LagSpikeCatcher();
boolean cullingDisabled = Config.Client.Graphics.AdvancedGraphics.disableDirectionalCulling.get();
Vec3d cameraPos = MC_RENDER.getCameraExactPosition();
DHBlockPos cameraBlockPos = MC_RENDER.getCameraBlockPosition();
Vec3f cameraDir = MC_RENDER.getLookAtVector();
int drawCount = 0;
//TODO: Directional culling
bufferHandler.render(this);
//if (drawCall==0)
// tickLogger.info("DrawCall Count: {}", drawCount);
//================//
// render cleanup //
//================//
draw.end("LodDraw");
profiler.popPush("LOD cleanup");
LagSpikeCatcher drawCleanup = new LagSpikeCatcher();
lightmap.unbind();
if (ENABLE_IBO) quadIBO.unbind();
GL32.glBindBuffer(GL32.GL_ARRAY_BUFFER, 0);
shaderProgram.unbind();
//lightmapTexture.free();
GL32.glClear(GL32.GL_DEPTH_BUFFER_BIT);
currentState.restore();
drawCleanup.end("LodDrawCleanup");
// end of internal LOD profiling
profiler.pop();
tickLogger.incLogTries();
}
//=================//
// Setup Functions //
//=================//
/** Setup all render objects - REQUIRES to be in render thread */
private void setup() {
if (isSetupComplete) {
EVENT_LOGGER.warn("Renderer setup called but it has already completed setup!");
return;
}
if (!GLProxy.hasInstance()) {
EVENT_LOGGER.warn("Renderer setup called but GLProxy has not yet been setup!");
return;
}
EVENT_LOGGER.info("Setting up renderer");
isSetupComplete = true;
shaderProgram = new LodRenderProgram(LodFogConfig.generateFogConfig());
if (ENABLE_IBO) {
quadIBO = new QuadElementBuffer();
quadIBO.reserve(RenderBuffer.MAX_QUADS_PER_BUFFER);
}
EVENT_LOGGER.info("Renderer setup complete");
}
private Color getFogColor(float partialTicks)
{
Color fogColor;
if (Config.Client.Graphics.FogQuality.fogColorMode.get() == EFogColorMode.USE_SKY_COLOR)
fogColor = MC_RENDER.getSkyColor();
else
fogColor = MC_RENDER.getFogColor(partialTicks);
return fogColor;
}
private Color getSpecialFogColor(float partialTicks)
{
return MC_RENDER.getSpecialFogColor(partialTicks);
}
//======================//
// Cleanup Functions //
//======================//
/** cleanup and free all render objects. REQUIRES to be in render thread
* (Many objects are Native, outside of JVM, and need manual cleanup) */
private void cleanup() {
if (!isSetupComplete) {
EVENT_LOGGER.warn("Renderer cleanup called but Renderer has not completed setup!");
return;
}
if (!GLProxy.hasInstance()) {
EVENT_LOGGER.warn("Renderer Cleanup called but the GLProxy has never been inited!");
return;
}
isSetupComplete = false;
EVENT_LOGGER.info("Renderer Cleanup Started");
shaderProgram.free();
if (quadIBO != null) quadIBO.destroy(false);
EVENT_LOGGER.info("Renderer Cleanup Complete");
}
}
@@ -0,0 +1,4 @@
package com.seibel.lod.core.a7.save.io;
public class LevelFileHandler {
}
@@ -0,0 +1,275 @@
package com.seibel.lod.core.a7.save.io;
import com.seibel.lod.core.config.Config;
import com.seibel.lod.core.handlers.dependencyInjection.SingletonInjector;
import com.seibel.lod.core.handlers.dimensionFinder.PlayerData;
import com.seibel.lod.core.handlers.dimensionFinder.SubDimCompare;
import com.seibel.lod.core.logging.ConfigBasedLogger;
import com.seibel.lod.core.objects.DHChunkPos;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
import org.apache.logging.log4j.LogManager;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
public class LevelToFileMatcher implements AutoCloseable {
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
public static final ConfigBasedLogger LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Client.Advanced.Debugging.DebugSwitch.logFileSubDimEvent.get());
private final ExecutorService matcherThread = LodUtil.makeSingleThreadPool("Level-To-File-Matcher");
private PlayerData playerData = null;
private PlayerData firstSeenPlayerData = null;
/** If true the LodDimensionFileHelper is attempting to determine the folder for this dimension */
private final AtomicBoolean determiningWorldFolder = new AtomicBoolean(false);
private final ILevelWrapper currentLevel;
private volatile File foundLevel = null;
private final File[] potentialFiles;
private final File levelsFolder;
public LevelToFileMatcher(ILevelWrapper targetWorld, File levelsFolder, File[] potentialFiles) {
this.currentLevel = targetWorld;
this.potentialFiles = potentialFiles;
this.levelsFolder = levelsFolder;
if (potentialFiles.length == 0) {
String newId = UUID.randomUUID().toString();
LOGGER.info("No potential level files found. Creating a new sub dimension with ID {}...",
LodUtil.shortenString(newId, 8));
foundLevel = new File(levelsFolder, newId);
}
}
// May return null, where at this moment the level is not yet known
public File tryGetLevel() {
tick();
return foundLevel;
}
public boolean isFindingLevel(ILevelWrapper level) {
return Objects.equals(level, currentLevel);
}
private void tick() {
if (foundLevel != null) return;
// prevent multiple threads running at the same time
if (determiningWorldFolder.getAndSet(true)) return;
matcherThread.submit(() ->
{
try {
// attempt to get the file handler
File saveDir = attemptToDetermineSubDimensionFolder();
if (saveDir != null) foundLevel = saveDir;
} catch (IOException e) {
LOGGER.error("Unable to set the dimension file handler for level [" + currentLevel + "]. Error: ", e);
} finally {
// make sure we unlock this method
determiningWorldFolder.set(false);
}
});
}
/**
* Currently this method checks a single chunk (where the player is)
* and compares it against the same chunk position in the other dimension worlds to
* guess which world the player is in.
*
* @throws IOException if the folder doesn't exist or can't be accessed
*/
public File attemptToDetermineSubDimensionFolder() throws IOException
{
{ // Update PlayerData
PlayerData data = PlayerData.tryGetPlayerData(MC_CLIENT);
if (data != null) {
if (firstSeenPlayerData == null) {
firstSeenPlayerData = data;
}
playerData = data;
}
}
// relevant positions
DHChunkPos playerChunkPos = new DHChunkPos(playerData.playerBlockPos);
int startingBlockPosX = playerChunkPos.getMinBlockX();
int startingBlockPosZ = playerChunkPos.getMinBlockZ();
// chunk from the newly loaded level
IChunkWrapper newlyLoadedChunk = MC_CLIENT.getWrappedClientWorld().tryGetChunk(playerChunkPos);
// check if this chunk is valid to test
if (!CanDetermineLevelFolder(newlyLoadedChunk))
return null;
//TODO: Compute a ChunkData from current chunk.
/*
// generate a LOD to test against
boolean lodGenerated = InternalApiShared.lodBuilder.generateLodNodeFromChunk(newlyLoadedDim, newlyLoadedChunk, new LodBuilderConfig(EDistanceGenerationMode.FULL), true, true);
if (!lodGenerated)
return null;
// log the start of this attempt
LOGGER.info("Attempting to determine sub-dimension for [" + MC_CLIENT.getCurrentDimension().getDimensionName() + "]");
LOGGER.info("Player block pos in dimension: [" + playerData.playerBlockPos.getX() + "," + playerData.playerBlockPos.getY() + "," + playerData.playerBlockPos.getZ() + "]");
// new chunk data
long[][][] newChunkData = new long[LodUtil.CHUNK_WIDTH][LodUtil.CHUNK_WIDTH][];
for (int x = 0; x < LodUtil.CHUNK_WIDTH; x++)
{
for (int z = 0; z < LodUtil.CHUNK_WIDTH; z++)
{
long[] array = newlyLoadedDim.getRegion(playerRegionPos.x, playerRegionPos.z).getAllData(LodUtil.BLOCK_DETAIL_LEVEL, x + startingBlockPosX, z + startingBlockPosZ);
newChunkData[x][z] = array;
}
}
boolean newChunkHasData = !isDataEmpty(newChunkData);
// check if the chunk is actually empty
if (!newChunkHasData)
{
if (newlyLoadedChunk.getHeight() != 0)
{
// the chunk isn't empty but the LOD is...
String message = "Error: the chunk at (" + playerChunkPos.getX() + "," + playerChunkPos.getZ() + ") has a height of [" + newlyLoadedChunk.getHeight() + "] but the LOD generated is empty!";
LOGGER.error(message);
}
else
{
String message = "Warning: The chunk at (" + playerChunkPos.getX() + "," + playerChunkPos.getZ() + ") is empty.";
LOGGER.warn(message);
}
return null;
}*/
// compare each world with the newly loaded one
SubDimCompare mostSimilarSubDim = null;
File[] levelFolders = potentialFiles;
LOGGER.info("Potential Sub Dimension folders: [" + levelFolders.length + "]");
for (File testLevelFolder : levelFolders)
{
LOGGER.info("Testing level folder: [" + LodUtil.shortenString(testLevelFolder.getName(), 8) + "]");
try
{
// TODO: Try load a data file overlapping the playerChunkPos from ClientOnlySaveStructure,
// and then use it to compare chunk data to current chunk.
/*
// get a LOD from this dimension folder
LodDimension tempLodDim = new LodDimension(null, 1, null, false);
tempLodDim.move(playerRegionPos);
LodDimensionFileHandler tempFileHandler = new LodDimensionFileHandler(testLevelFolder, tempLodDim);
LodRegion testRegion = tempFileHandler.loadRegionFromFile(LodUtil.BLOCK_DETAIL_LEVEL, playerRegionPos, VERTICAL_QUALITY_TO_TEST_WITH);
// get data from this LOD
long[][][] testChunkData = new long[LodUtil.CHUNK_WIDTH][LodUtil.CHUNK_WIDTH][];
for (int x = 0; x < LodUtil.CHUNK_WIDTH; x++)
{
for (int z = 0; z < LodUtil.CHUNK_WIDTH; z++)
{
long[] array = testRegion.getAllData(LodUtil.BLOCK_DETAIL_LEVEL, x + startingBlockPosX, z + startingBlockPosZ);
testChunkData[x][z] = array;
}
}
// get the player data for this dimension folder
PlayerData testPlayerData = new PlayerData(testLevelFolder);
LOGGER.info("Last known player pos: [" + testPlayerData.playerBlockPos.getX() + "," + testPlayerData.playerBlockPos.getY() + "," + testPlayerData.playerBlockPos.getZ() + "]");
// check if the block positions are close
int playerBlockDist = testPlayerData.playerBlockPos.getManhattanDistance(playerData.playerBlockPos);
LOGGER.info("Player block position distance between saved sub dimension and first seen is [" + playerBlockDist + "]");
// check if the chunk is actually empty
if (isDataEmpty(testChunkData))
{
String message = "The test chunk for dimension folder [" + LodUtil.shortenString(testLevelFolder.getName(), 8) + "] and chunk pos (" + playerChunkPos.getX() + "," + playerChunkPos.getZ() + ") is empty. This is expected if the position is outside the sub-dimension's generated area.";
LOGGER.info(message);
continue;
}
// compare the two LODs
int equalDataPoints = 0;
int totalDataPointCount = 0;
for (int x = 0; x < LodUtil.CHUNK_WIDTH; x++)
{
for (int z = 0; z < LodUtil.CHUNK_WIDTH; z++)
{
for (int y = 0; y < newChunkData[x][z].length; y++)
{
if (newChunkData[x][z][y] == testChunkData[x][z][y])
{
equalDataPoints++;
}
totalDataPointCount++;
if (!DataPointUtil.doesItExist(newChunkData[x][z][y]) || !DataPointUtil.doesItExist(testChunkData[x][z][y]))
break;
}
}
}
// determine if this world is closer to the newly loaded world
SubDimCompare subDimCompare = new SubDimCompare(equalDataPoints, totalDataPointCount, playerBlockDist, testLevelFolder);
if (mostSimilarSubDim == null || subDimCompare.compareTo(mostSimilarSubDim) > 0)
{
mostSimilarSubDim = subDimCompare;
}
LOGGER.info("Sub dimension [" + LodUtil.shortenString(testLevelFolder.getName(), 8) + "...] is current dimension probability: " + LodUtil.shortenString(subDimCompare.getPercentEqual() + "", 5) + " (" + equalDataPoints + "/" + totalDataPointCount + ")");
*/
}
catch (Exception e)
{
// this sub dimension isn't formatted correctly
// for now we are just assuming it is an unrelated file
}
}
// TODO if two sub dimensions contain the same LODs merge them???
// the first seen player data is no longer needed, the sub dimension has been determined
firstSeenPlayerData = null;
if (mostSimilarSubDim != null && mostSimilarSubDim.isValidSubDim())
{
// we found a world folder that is similar, use it
LOGGER.info("Sub Dimension set to: [" + LodUtil.shortenString(mostSimilarSubDim.folder.getName(), 8) + "...] with an equality of [" + mostSimilarSubDim.getPercentEqual() + "]");
return mostSimilarSubDim.folder;
}
else
{
// no world folder was found, create a new one
double highestEqualityPercent = mostSimilarSubDim != null ? mostSimilarSubDim.getPercentEqual() : 0;
String newId = UUID.randomUUID().toString();
String message = "No suitable sub dimension found. The highest equality was [" + LodUtil.shortenString(highestEqualityPercent + "", 5) + "]. Creating a new sub dimension with ID: " + LodUtil.shortenString(newId, 8) + "...";
LOGGER.info(message);
File folder = new File(levelsFolder, newId);
folder.mkdirs();
return folder;
}
}
/** Returns true if the given chunk is valid to test */
public boolean CanDetermineLevelFolder(IChunkWrapper chunk)
{
// we can only guess if the given chunk can be converted into a LOD
return false; //FIXME: Fix this after LodBUilder is done.
//return LodBuilder.canGenerateLodFromChunk(chunk);
}
@Override
public void close() {
matcherThread.shutdownNow();
}
}
@@ -0,0 +1,199 @@
package com.seibel.lod.core.a7.save.io;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
import java.util.zip.Adler32;
import java.util.zip.CheckedOutputStream;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.a7.util.UnclosableOutputStream;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.util.LodUtil;
import org.apache.logging.log4j.Logger;
public class MetaFile {
public static final Logger LOGGER = DhLoggerBuilder.getLogger();
//Metadata format:
//
// 4 bytes: magic bytes: "DHv0" (in ascii: 0x44 48 76 30) (this also signal the metadata format)
// 4 bytes: section X position
// 4 bytes: section Y position (Unused, for future proofing)
// 4 bytes: section Z position
//
// 4 bytes: data checksum //TODO: Implement checksum
// 1 byte: section detail level
// 1 byte: data detail level // Note: not sure if this is needed
// 1 byte: loader version
// 1 byte: unused
//
// 8 bytes: datatype identifier
//
// 8 bytes: timestamp
// Used size: 40 bytes
// Remaining space: 24 bytes
// Total size: 64 bytes
public static final int METADATA_SIZE = 64;
public static final int METADATA_RESERVED_SIZE = 24;
public static final int METADATA_MAGIC_BYTES = 0x44_48_76_30;
// Currently set to false because for some reason Window is throwing PermissionDeniedException when trying to atomic replace a file...
public static final boolean USE_ATOMIC_MOVE_REPLACE = false;
public final DhSectionPos pos;
public File path;
public int checksum;
public long timestamp;
public byte dataLevel;
//Loader stuff
public long dataTypeId;
public byte loaderVersion;
private final ReentrantReadWriteLock assertLock = new ReentrantReadWriteLock();
// Load a metaFile in this path. It also automatically read the metadata.
protected MetaFile(File path) throws IOException {
this.path = path;
validateFile();
LodUtil.assertTrue(assertLock.readLock().tryLock());
try (FileChannel channel = FileChannel.open(path.toPath(), StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(METADATA_SIZE);
channel.read(buffer, 0);
channel.close();
buffer.flip();
this.path = path;
int magic = buffer.getInt();
if (magic != METADATA_MAGIC_BYTES) {
throw new IOException("Invalid file: Magic bytes check failed.");
}
int x = buffer.getInt();
int y = buffer.getInt(); // Unused
int z = buffer.getInt();
checksum = buffer.getInt();
byte detailLevel = buffer.get();
dataLevel = buffer.get();
loaderVersion = buffer.get();
byte unused = buffer.get();
dataTypeId = buffer.getLong();
timestamp = buffer.getLong();
LodUtil.assertTrue(buffer.remaining() == METADATA_RESERVED_SIZE);
pos = new DhSectionPos(detailLevel, x, z);
} finally {
assertLock.readLock().unlock();
}
}
// Make a new MetaFile. It doesn't load or write any metadata itself.
protected MetaFile(File path, DhSectionPos pos) {
this.path = path;
this.pos = pos;
}
private void validateFile() throws IOException {
if (!path.exists()) throw new IOException("File missing");
if (!path.isFile()) throw new IOException("Not a file");
if (!path.canRead()) throw new IOException("File not readable");
if (!path.canWrite()) throw new IOException("File not writable");
}
protected void updateMetaData() throws IOException {
validateFile();
LodUtil.assertTrue(assertLock.readLock().tryLock());
try (FileChannel channel = FileChannel.open(path.toPath(), StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(METADATA_SIZE);
channel.read(buffer, 0);
channel.close();
buffer.flip();
int magic = buffer.getInt();
if (magic != METADATA_MAGIC_BYTES) {
throw new IOException("Invalid file: Magic bytes check failed.");
}
int x = buffer.getInt();
int y = buffer.getInt(); // Unused
int z = buffer.getInt();
checksum = buffer.getInt();
byte detailLevel = buffer.get();
dataLevel = buffer.get();
byte loaderVersion = buffer.get();
byte unused = buffer.get();
dataTypeId = buffer.getLong();
timestamp = buffer.getLong();
LodUtil.assertTrue(buffer.remaining() == METADATA_RESERVED_SIZE);
DhSectionPos newPos = new DhSectionPos(detailLevel, x, z);
if (!newPos.equals(pos)) {
throw new IOException("Invalid file: Section position changed.");
}
this.loaderVersion = loaderVersion;
} finally {
assertLock.readLock().unlock();
}
}
protected void writeData(Consumer<OutputStream> dataWriter) throws IOException {
if (path.exists()) validateFile();
File writerFile;
if (USE_ATOMIC_MOVE_REPLACE) {
writerFile = new File(path.getPath() + ".tmp");
writerFile.deleteOnExit();
} else {
writerFile = path;
}
LodUtil.assertTrue(assertLock.writeLock().tryLock());
try (FileChannel file = FileChannel.open(writerFile.toPath(),
StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
{
file.position(METADATA_SIZE);
int checksum;
try (OutputStream channelOut = new UnclosableOutputStream(Channels.newOutputStream(file)); // Prevent closing the channel
BufferedOutputStream bufferedOut = new BufferedOutputStream(channelOut); // TODO: Is default buffer size ok? Do we even need to buffer?
CheckedOutputStream checkedOut = new CheckedOutputStream(bufferedOut, new Adler32())) { // TODO: Is Adler32 ok?
dataWriter.accept(checkedOut);
checksum = (int) checkedOut.getChecksum().getValue();
}
file.position(0);
// Write metadata
ByteBuffer buff = ByteBuffer.allocate(METADATA_SIZE);
buff.putInt(METADATA_MAGIC_BYTES);
buff.putInt(pos.sectionX);
buff.putInt(Integer.MIN_VALUE); // Unused
buff.putInt(pos.sectionZ);
buff.putInt(checksum);
buff.put(pos.sectionDetail);
buff.put(dataLevel);
buff.put(loaderVersion);
buff.put(Byte.MIN_VALUE); // Unused
buff.putLong(dataTypeId);
buff.putLong(timestamp);
LodUtil.assertTrue(buff.remaining() == METADATA_RESERVED_SIZE);
buff.flip();
file.write(buff);
}
file.close();
if (USE_ATOMIC_MOVE_REPLACE) {
// Atomic move / replace the actual file
Files.move(writerFile.toPath(), path.toPath(), StandardCopyOption.ATOMIC_MOVE);
}
} finally {
assertLock.writeLock().unlock();
try {
if (USE_ATOMIC_MOVE_REPLACE && writerFile.exists()) {
boolean i = writerFile.delete(); // Delete temp file. Ignore errors if fails.
}
} catch (Exception ignored) {}
}
}
}
@@ -0,0 +1,314 @@
package com.seibel.lod.core.a7.save.io.file;
import com.google.common.collect.HashMultimap;
import com.seibel.lod.core.a7.datatype.LodDataSource;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.datatype.full.FullDataSource;
import com.seibel.lod.core.a7.level.ILevel;
import com.seibel.lod.core.a7.level.IServerLevel;
import com.seibel.lod.core.a7.pos.DhLodPos;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.util.LodUtil;
import org.apache.logging.log4j.Logger;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
public class DataFileHandler implements IDataSourceProvider {
// Note: Single main thread only for now. May make it multi-thread later, depending on the usage.
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
final ExecutorService fileReaderThread = LodUtil.makeThreadPool(4, "FileReaderThread");
final ConcurrentHashMap<DhSectionPos, DataMetaFile> files = new ConcurrentHashMap<>();
final ILevel level;
final File saveDir;
AtomicInteger topDetailLevel = new AtomicInteger(-1);
final int minDetailLevel = FullDataSource.SECTION_SIZE_OFFSET;
final Function<DhSectionPos, CompletableFuture<LodDataSource>> dataSourceCreator;
public DataFileHandler(ILevel level, File saveRootDir,
Function<DhSectionPos, CompletableFuture<LodDataSource>> dataSourceCreator) {
this.saveDir = saveRootDir;
this.level = level;
this.dataSourceCreator = dataSourceCreator;
}
/*
* Caller must ensure that this method is called only once,
* and that this object is not used before this method is called.
*/
@Override
public void addScannedFile(Collection<File> detectedFiles) {
HashMultimap<DhSectionPos, DataMetaFile> filesByPos = HashMultimap.create();
LOGGER.info("Detected {} valid files in {}", detectedFiles.size(), saveDir);
{ // Sort files by pos.
for (File file : detectedFiles) {
try {
DataMetaFile metaFile = new DataMetaFile(level, file);
filesByPos.put(metaFile.pos, metaFile);
} catch (IOException e) {
LOGGER.error("Failed to read file {}. File will be deleted.", file, e);
if (!file.delete()) {
LOGGER.error("Failed to delete file {}.", file);
}
}
}
}
// Warn for multiple files with the same pos, and then select the one with latest timestamp.
for (DhSectionPos pos : filesByPos.keySet()) {
Collection<DataMetaFile> metaFiles = filesByPos.get(pos);
DataMetaFile fileToUse;
if (metaFiles.size() > 1) {
fileToUse = Collections.max(metaFiles, Comparator.comparingLong(a -> a.timestamp));
{
StringBuilder sb = new StringBuilder();
sb.append("Multiple files with the same pos: ");
sb.append(pos);
sb.append("\n");
for (DataMetaFile metaFile : metaFiles) {
sb.append("\t");
sb.append(metaFile.path);
sb.append("\n");
}
sb.append("\tUsing: ");
sb.append(fileToUse.path);
sb.append("\n");
sb.append("(Other files will be renamed by appending \".old\" to their name.)");
LOGGER.warn(sb.toString());
// Rename all other files with the same pos to .old
for (DataMetaFile metaFile : metaFiles) {
if (metaFile == fileToUse) continue;
File oldFile = new File(metaFile.path + ".old");
try {
if (!metaFile.path.renameTo(oldFile)) throw new RuntimeException("Renaming failed");
} catch (Exception e) {
LOGGER.error("Failed to rename file: " + metaFile.path + " to " + oldFile, e);
}
}
}
} else {
fileToUse = metaFiles.iterator().next();
}
// Add file to the list of files.
topDetailLevel.updateAndGet(v -> Math.max(v, fileToUse.pos.sectionDetail));
files.put(pos, fileToUse);
}
}
private DataMetaFile atomicGetOrMakeFile(DhSectionPos pos) {
DataMetaFile metaFile = files.get(pos);
if (metaFile == null) {
File file = computeDefaultFilePath(pos);
//FIXME: Handle file already exists issue. Possibly by renaming the file.
LodUtil.assertTrue(!file.exists(), "File {} already exist for path {}", file, pos);
CompletableFuture<LodDataSource> gen = new CompletableFuture<>();
DataMetaFile newMetaFile = new DataMetaFile(level, file, pos, gen);
metaFile = files.putIfAbsent(pos, newMetaFile); // This is a CAS with expected null value.
if (metaFile == null) {
buildFile(pos, gen);
metaFile = newMetaFile;
} else {
gen.cancel(true);
}
}
return metaFile;
}
private void selfSearch(DhSectionPos basePos, DhSectionPos pos, ArrayList<DataMetaFile> existFiles, ArrayList<DhSectionPos> missing) {
byte detail = pos.sectionDetail;
boolean allEmpty = true;
outerLoop:
while (--detail >= minDetailLevel) {
DhLodPos min = pos.getCorner().getCorner(detail);
int count = pos.getSectionBBoxPos().getWidth(detail);
for (int ox = 0; ox<count; ox++) {
for (int oz = 0; oz<count; oz++) {
DhSectionPos subPos = new DhSectionPos(detail, ox+min.x, oz+min.z);
LodUtil.assertTrue(pos.overlaps(basePos) && subPos.overlaps(pos));
//TODO: The following check is temp as we only samples corner points per data, which means
// on a very different level, we may not need the entire section at all.
if (!FullDataSource.neededForPosition(basePos, subPos)) continue;
if (files.containsKey(subPos)) {
allEmpty = false;
break outerLoop;
}
}
}
}
if (allEmpty) {
missing.add(pos);
} else {
{
DhSectionPos childPos = pos.getChild(0);
if (FullDataSource.neededForPosition(basePos, childPos)) {
DataMetaFile metaFile = files.get(childPos);
if (metaFile != null) {
existFiles.add(metaFile);
} else if (childPos.sectionDetail == minDetailLevel) {
missing.add(childPos);
} else {
selfSearch(basePos, childPos, existFiles, missing);
}
}
}
{
DhSectionPos childPos = pos.getChild(1);
if (FullDataSource.neededForPosition(basePos, childPos)) {
DataMetaFile metaFile = files.get(childPos);
if (metaFile != null) {
existFiles.add(metaFile);
} else if (childPos.sectionDetail == minDetailLevel) {
missing.add(childPos);
} else {
selfSearch(basePos, childPos, existFiles, missing);
}
}
}
{
DhSectionPos childPos = pos.getChild(2);
if (FullDataSource.neededForPosition(basePos, childPos)) {
DataMetaFile metaFile = files.get(childPos);
if (metaFile != null) {
existFiles.add(metaFile);
} else if (childPos.sectionDetail == minDetailLevel) {
missing.add(childPos);
} else {
selfSearch(basePos, childPos, existFiles, missing);
}
}
}
{
DhSectionPos childPos = pos.getChild(3);
if (FullDataSource.neededForPosition(basePos, childPos)) {
DataMetaFile metaFile = files.get(childPos);
if (metaFile != null) {
existFiles.add(metaFile);
} else if (childPos.sectionDetail == minDetailLevel) {
missing.add(childPos);
} else {
selfSearch(basePos, childPos, existFiles, missing);
}
}
}
}
}
private void buildFile(DhSectionPos pos, CompletableFuture<LodDataSource> gen) {
ArrayList<DataMetaFile> existFiles = new ArrayList<>();
ArrayList<DhSectionPos> missing = new ArrayList<>();
selfSearch(pos, pos, existFiles, missing);
LodUtil.assertTrue(!missing.isEmpty() || !existFiles.isEmpty());
if (missing.size() == 1 && existFiles.isEmpty() && missing.get(0).equals(pos)) {
dataSourceCreator.apply(pos).whenComplete((f, ex) -> {
if (ex != null) {
gen.completeExceptionally(ex);
} else {
gen.complete(f);
}
});
return;
}
LOGGER.info("Creating file at {} using {} existing files and {} new files.", pos, existFiles.size(), missing.size());
ArrayList<CompletableFuture<LodDataSource>> futures = new ArrayList<>(existFiles.size() + missing.size());
for (DhSectionPos missingPos : missing) {
existFiles.add(atomicGetOrMakeFile(missingPos));
}
FullDataSource fullDataSource = FullDataSource.createEmpty(pos);
for (DataMetaFile metaFile : existFiles) {
futures.add(
metaFile.loadOrGetCached(fileReaderThread).whenComplete((data, ex) -> {
if (ex != null) return;
if (!(data instanceof FullDataSource)) return;
fullDataSource.writeFromLower((FullDataSource) data);
})
);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.whenComplete((v, ex) -> {
if (ex != null) {
gen.completeExceptionally(ex);
} else {
gen.complete(fullDataSource);
}
});
}
/*
* This call is concurrent. I.e. it supports multiple threads calling this method at the same time.
*/
@Override
public CompletableFuture<LodDataSource> read(DhSectionPos pos) {
topDetailLevel.updateAndGet(v -> Math.max(v, pos.sectionDetail));
DataMetaFile metaFile = atomicGetOrMakeFile(pos);
return metaFile.loadOrGetCached(fileReaderThread);
}
/*
* This call is concurrent. I.e. it supports multiple threads calling this method at the same time.
*/
@Override
public void write(DhSectionPos sectionPos, ChunkSizedData chunkData) {
DhLodPos chunkPos = new DhLodPos((byte) (chunkData.dataDetail+4), chunkData.x, chunkData.z);
LodUtil.assertTrue(chunkPos.overlaps(sectionPos.getSectionBBoxPos()), "Chunk {} does not overlap section {}", chunkPos, sectionPos);
chunkPos = chunkPos.convertUpwardsTo((byte) minDetailLevel); // TODO: Handle if chunkData has higher detail than lowestDetail.
recursiveWrite(new DhSectionPos(chunkPos.detail, chunkPos.x, chunkPos.z), chunkData);
}
private void recursiveWrite(DhSectionPos sectionPos, ChunkSizedData chunkData) {
DataMetaFile metaFile = files.get(sectionPos);
if (metaFile != null) { // Fast path: if there is a file for this section, just write to it.
metaFile.addToWriteQueue(chunkData);
}
if (sectionPos.sectionDetail <= topDetailLevel.get()) {
recursiveWrite(sectionPos.getParent(), chunkData);
}
}
/*
* This call is concurrent. I.e. it supports multiple threads calling this method at the same time.
*/
@Override
public CompletableFuture<Void> flushAndSave() {
ArrayList<CompletableFuture<Void>> futures = new ArrayList<>();
for (DataMetaFile metaFile : files.values()) {
futures.add(metaFile.flushAndSave(fileReaderThread));
}
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
}
@Override
public boolean isCacheValid(DhSectionPos sectionPos, long timestamp) {
DataMetaFile file = files.get(sectionPos);
if (file == null) return false;
//TODO
return true;
}
private File computeDefaultFilePath(DhSectionPos pos) { //TODO: Temp code as we haven't decided on the file naming & location yet.
return new File(saveDir, pos.serialize() + ".lod");
}
@Override
public void close() {
DataMetaFile.debugCheck();
//TODO
}
}
@@ -0,0 +1,351 @@
package com.seibel.lod.core.a7.save.io.file;
import java.awt.*;
import java.io.*;
import java.lang.ref.*;
import java.security.Provider;
import java.sql.Ref;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
import java.util.function.Supplier;
import com.seibel.lod.core.a7.datatype.LodDataSource;
import com.seibel.lod.core.a7.datatype.DataSourceLoader;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.datatype.full.FullDataSource;
import com.seibel.lod.core.a7.pos.DhLodPos;
import com.seibel.lod.core.a7.save.io.MetaFile;
import com.seibel.lod.core.a7.level.ILevel;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.util.LodUtil;
public class DataMetaFile extends MetaFile {
private final ILevel level;
public DataSourceLoader loader;
public Class<? extends LodDataSource> dataType;
AtomicInteger localVersion = new AtomicInteger(); // This MUST be atomic
// The '?' type should either be:
// SoftReference<LodDataSource>, or - Non-dirty file that can be GCed
// CompletableFuture<LodDataSource>, or - File that is being loaded
// null - Nothing is loaded or being loaded
AtomicReference<Object> data = new AtomicReference<Object>(null);
//TODO: use ConcurrentAppendSingleSwapContainer<LodDataSource> instead of below:
private static class GuardedMultiAppendQueue {
ReentrantReadWriteLock appendLock = new ReentrantReadWriteLock();
ConcurrentLinkedQueue<ChunkSizedData> queue = new ConcurrentLinkedQueue<>();
}
AtomicReference<GuardedMultiAppendQueue> writeQueue =
new AtomicReference<>(new GuardedMultiAppendQueue());
GuardedMultiAppendQueue _backQueue = new GuardedMultiAppendQueue();
private final AtomicBoolean inCacheWriteLock = new AtomicBoolean(false);
private static final ReferenceQueue<LodDataSource> lifeCycleDebugQueue = new ReferenceQueue<>();
private static final Set<DataObjTracker> lifeCycleDebugSet = ConcurrentHashMap.newKeySet();
private static class DataObjTracker extends PhantomReference<LodDataSource> implements Closeable {
private final DhSectionPos pos;
DataObjTracker(LodDataSource data) {
super(data, lifeCycleDebugQueue);
LOGGER.info("Phantom created on {}! count: {}", data.getSectionPos(), lifeCycleDebugSet.size());
lifeCycleDebugSet.add(this);
pos = data.getSectionPos();
}
@Override
public void close() {
lifeCycleDebugSet.remove(this);
}
}
public void addToWriteQueue(ChunkSizedData datatype) {
debugCheck();
DhLodPos chunkPos = new DhLodPos((byte) (datatype.dataDetail + 4), datatype.x, datatype.z);
LodUtil.assertTrue(pos.getSectionBBoxPos().overlaps(chunkPos), "Chunk pos {} doesn't overlap with section {}", chunkPos, pos);
GuardedMultiAppendQueue queue = writeQueue.get();
// Using read lock is OK, because the queue's underlying data structure is thread-safe.
// This lock is only used to insure on polling the queue, that the queue is not being
// modified by another thread.
Lock appendLock = queue.appendLock.readLock();
appendLock.lock();
try {
queue.queue.add(datatype);
} finally {
appendLock.unlock();
}
}
private void swapWriteQueue() {
GuardedMultiAppendQueue queue = writeQueue.getAndSet(_backQueue);
// Acquire write lock and then release it again as we only need to ensure that the queue
// is not being appended to by another thread. Note that the above atomic swap &
// the guarantee that all append first acquire the appendLock means after the locK() call,
// there will be no other threads able to or is currently appending to the queue.
// Note: The above needs the getAndSet() to have at least Release Memory order.
// (not that java supports anything non volatile for getAndSet()...)
queue.appendLock.writeLock().lock();
queue.appendLock.writeLock().unlock();
_backQueue = queue;
}
// Load a metaFile in this path. It also automatically read the metadata.
public DataMetaFile(ILevel level, File path) throws IOException {
super(path);
debugCheck();
this.level = level;
loader = DataSourceLoader.getLoader(dataTypeId, loaderVersion);
if (loader == null) {
throw new IOException("Invalid file: Data type loader not found: "
+ dataTypeId + "(v" + loaderVersion + ")");
}
dataType = loader.clazz;
}
// Make a new MetaFile. It doesn't load or write any metadata itself.
public DataMetaFile(ILevel level, File path, DhSectionPos pos, CompletableFuture<LodDataSource> creator) {
super(path, pos);
debugCheck();
this.level = level;
CompletableFuture<LodDataSource> future = new CompletableFuture<>();
data.set(future);
creator.thenApply((f) -> {
applyWriteQueue(f);
return f;
}).whenComplete((f, e) -> {
if (e != null) {
LOGGER.error("Uncaught error on creation {}: ", path, e);
future.complete(null);
data.set(null);
} else {
future.complete(f);
new DataObjTracker(f);
data.set(new SoftReference<>(f));
}
});
}
public boolean isValid(int version) {
debugCheck();
boolean isValid;
// First check if write queue is empty, then check if localVersion is equal to version.
// Must be done in this order as writer will increment localVersion before polling in the write queue.
// Note: Be careful with the localVerion read's memory order if we do switch over to java 1.9.
// It should be acquire or higher!
isValid = writeQueue.get().queue.isEmpty(); // The 'get()' & 'isEmpty()' enforce a memory barrier.
// Also, we are just querying the state, and this means no
// need to get any locks for the queue.
isValid &= localVersion.get() == version; // The 'get()' enforce a memory barrier.
return isValid;
}
// "unchecked": Suppress casting of CompletableFuture<?> to CompletableFuture<LodDataSource>
// "PointlessBooleanExpression": Suppress explicit (boolean == false) check for more understandable CAS operation code.
@SuppressWarnings({"unchecked", "PointlessBooleanExpression"})
private CompletableFuture<LodDataSource> _readCached(Object obj) {
// Has file cached in RAM and not freed yet.
if ((obj instanceof SoftReference<?>)) {
Object inner = ((SoftReference<?>)obj).get();
if (inner != null) {
LodUtil.assertTrue(inner instanceof LodDataSource);
boolean isEmpty = writeQueue.get().queue.isEmpty();
// If the queue is empty, and the CAS on inCacheWriteLock succeeds, then we are the thread
// that will be applying the changes to the cache.
if (!isEmpty) {
// Do a CAS on inCacheWriteLock to ensure that we are the only thread that is writing to the cache,
// or if we fail, then that means someone else is already doing it, and we can just continue.
// FIXME: Should we return a future that waits for the write to be done for CAS fail? Or should we just return the
// cached data that doesn't have all writes done immediately?
// The latter give us immediate access to the data, but we need to ensure concurrent reads and
// writes doesn't cause unexpected behavior down the line.
// For now, I'll go for the latter option and just hope nothing goes wrong...
if (inCacheWriteLock.getAndSet(true) == false) {
try {
applyWriteQueue((LodDataSource) inner);
} catch (Exception e) {
LOGGER.error("Error while applying changes to LodDataSource at {}: ", pos, e);
} finally {
inCacheWriteLock.set(false);
}
}
}
// Finally, return the cached data.
return CompletableFuture.completedFuture((LodDataSource)inner);
}
}
//==== Cached file out of scrope. ====
// Someone is already trying to complete it. so just return the obj.
if ((obj instanceof CompletableFuture<?>)) {
return (CompletableFuture<LodDataSource>)obj;
}
return null;
}
// Cause: Generic Type runtime casting cannot safety check it.
// However, the Union type ensures the 'data' should only contain the listed type.
public CompletableFuture<LodDataSource> loadOrGetCached(Executor fileReaderThreads) {
debugCheck();
Object obj = data.get();
CompletableFuture<LodDataSource> cached = _readCached(obj);
if (cached != null) return cached;
CompletableFuture<LodDataSource> future = new CompletableFuture<>();
// Would use faster and non-nesting Compare and exchange. But java 8 doesn't have it! :(
boolean worked = data.compareAndSet(obj, future);
if (!worked) return loadOrGetCached(fileReaderThreads);
// Would use CompletableFuture.completeAsync(...), But, java 8 doesn't have it! :(
//return future.completeAsync(this::loadAndUpdateDataSource, fileReaderThreads);
CompletableFuture.supplyAsync(this::loadAndUpdateDataSource, fileReaderThreads)
.whenComplete((f, e) -> {
if (e != null) {
LOGGER.error("Uncaught error loading file {}: ", path, e);
future.complete(null);
data.set(null);
} else {
future.complete(f);
new DataObjTracker(f);
data.set(new SoftReference<>(f));
}
});
return future;
}
// Return whether any write has happened to the data
private void applyWriteQueue(LodDataSource data) {
// Poll the write queue
// First check if write queue is empty, then swap the write queue.
// Must be done in this order to ensure isValid work properly. See isValid() for details.
boolean isEmpty = writeQueue.get().queue.isEmpty();
int localVer;
if (!isEmpty) {
localVer = localVersion.incrementAndGet();
swapWriteQueue();
int count = _backQueue.queue.size();
for (ChunkSizedData chunk : _backQueue.queue) {
data.update(chunk);
}
_backQueue.queue.clear();
write(data);
LOGGER.info("Updated Data file at {} for sect {} with {} chunk writes.", path, pos, count);
} else localVer = localVersion.get();
data.setLocalVersion(localVer);
}
private LodDataSource loadAndUpdateDataSource() {
LodDataSource data = loadFile();
if (data == null) data = FullDataSource.createEmpty(pos);
// Apply the write queue
LodUtil.assertTrue(!inCacheWriteLock.get(),"No one should be writing to the cache while we are in the process of " +
"loading one into the cache! Is this a deadlock?");
applyWriteQueue(data);
// Finally, return the data.
return data;
}
private LodDataSource loadFile() {
if (!path.exists()) return null;
// Refresh the metadata.
try {
super.updateMetaData();
} catch (Exception e) {
LOGGER.warn("Metadata for file {} changed unexpectedly and in an invalid state. Dropping file.", path, e);
return null;
}
if (loader == null) {
//LOGGER.warn("No loader for file {}. Dropping file.", path); // Disable as data lod has no loader yet.
return null;
}
// Load the file.
try (FileInputStream fio = getDataContent()){
return loader.loadData(this, fio, level);
} catch (Exception e) {
LOGGER.warn("Failed to load file {}. Dropping file.", path, e);
return null;
}
}
private FileInputStream getDataContent() throws IOException {
FileInputStream fin = new FileInputStream(path);
int toSkip = METADATA_SIZE;
while (toSkip > 0) {
long skipped = fin.skip(toSkip);
if (skipped == 0) {
throw new IOException("Invalid file: Failed to skip metadata.");
}
toSkip -= skipped;
}
if (toSkip != 0) {
throw new IOException("File IO Error: Failed to skip metadata.");
}
return fin;
}
public CompletableFuture<Void> flushAndSave(Executor fileWriterThreads) {
debugCheck();
boolean isEmpty = writeQueue.get().queue.isEmpty();
if (!isEmpty) {
return loadOrGetCached(fileWriterThreads).thenApply((unused) -> null); // This will flush the data to disk.
} else {
return CompletableFuture.completedFuture(null);
}
}
@Override
protected void updateMetaData() throws IOException {
super.updateMetaData();
loader = DataSourceLoader.getLoader(dataTypeId, loaderVersion);
if (loader == null) {
throw new IOException("Invalid file: Data type loader not found: " + dataTypeId + "(v" + loaderVersion + ")");
}
dataType = loader.clazz;
dataTypeId = loader.datatypeId;
}
private void write(LodDataSource data) {
try {
dataLevel = data.getDataDetail();
loader = DataSourceLoader.getLoader(data.getClass(), data.getDataVersion());
// FIXME: Uncomment this and fix id when we have FullDataSource loader!
//LodUtil.assertTrue(loader != null, "No loader for {} (v{})", data.getClass(), data.getDataVersion());
dataType = data.getClass();
dataTypeId = loader == null ? 0 : loader.datatypeId;
loaderVersion = data.getDataVersion();
timestamp = System.currentTimeMillis(); // TODO: Do we need to use server synced time?
// Warn: This may become an attack vector! Be careful!
super.writeData((out) -> {
try {
data.saveData(level, this, out);
} catch (IOException e) {
LOGGER.error("Failed to save data for file {}", path, e);
}
});
} catch (IOException e) {
LOGGER.error("Failed to write data for file {}", path, e);
}
}
public static void debugCheck() {
DataObjTracker phantom = (DataObjTracker) lifeCycleDebugQueue.poll();
while (phantom != null) {
LOGGER.info("Data {} is freed. {} remaining", phantom.pos, lifeCycleDebugSet.size());
phantom.close();
phantom = (DataObjTracker) lifeCycleDebugQueue.poll();
}
}
}
@@ -0,0 +1,12 @@
package com.seibel.lod.core.a7.save.io.file;
import com.seibel.lod.core.a7.generation.GenerationQueue;
import com.seibel.lod.core.a7.level.IServerLevel;
import java.io.File;
public class GeneratedDataFileHandler extends DataFileHandler {
public GeneratedDataFileHandler(IServerLevel level, File saveRootDir, GenerationQueue queue) {
super(level, saveRootDir, queue::generate);
}
}
@@ -0,0 +1,21 @@
package com.seibel.lod.core.a7.save.io.file;
import com.seibel.lod.core.a7.datatype.LodDataSource;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.datatype.full.FullFormat;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.objects.DHChunkPos;
import java.io.File;
import java.util.Collection;
import java.util.concurrent.CompletableFuture;
public interface IDataSourceProvider extends AutoCloseable {
void addScannedFile(Collection<File> detectedFiles);
CompletableFuture<LodDataSource> read(DhSectionPos pos);
void write(DhSectionPos sectionPos, ChunkSizedData chunkData);
CompletableFuture<Void> flushAndSave();
boolean isCacheValid(DhSectionPos sectionPos, long timestamp);
}
@@ -0,0 +1,15 @@
package com.seibel.lod.core.a7.save.io.file;
import com.seibel.lod.core.a7.level.ILevel;
import com.seibel.lod.core.util.LodUtil;
import java.io.File;
public class RemoteDataFileHandler extends DataFileHandler {
public RemoteDataFileHandler(ILevel level, File saveRootDir) {
super(level, saveRootDir, (pos) -> {
LodUtil.assertNotReach("TODO");
return null;
});
}
}
@@ -0,0 +1,16 @@
package com.seibel.lod.core.a7.save.io.render;
import com.seibel.lod.core.a7.datatype.LodRenderSource;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import java.io.File;
import java.util.Collection;
import java.util.concurrent.CompletableFuture;
public interface IRenderSourceProvider extends AutoCloseable {
CompletableFuture<LodRenderSource> read(DhSectionPos pos);
void addScannedFile(Collection<File> detectedFiles);
void write(DhSectionPos sectionPos, ChunkSizedData chunkData);
CompletableFuture<Void> flushAndSave();
}
@@ -0,0 +1,171 @@
package com.seibel.lod.core.a7.save.io.render;
import com.google.common.collect.HashMultimap;
import com.seibel.lod.core.a7.datatype.PlaceHolderRenderSource;
import com.seibel.lod.core.a7.datatype.LodRenderSource;
import com.seibel.lod.core.a7.datatype.column.ColumnRenderSource;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.level.IClientLevel;
import com.seibel.lod.core.a7.pos.DhLodPos;
import com.seibel.lod.core.a7.save.io.file.IDataSourceProvider;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.util.LodUtil;
import org.apache.logging.log4j.Logger;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
public class RenderFileHandler implements IRenderSourceProvider {
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
final ExecutorService renderCacheThread = LodUtil.makeSingleThreadPool("RenderCacheThread");
final ConcurrentHashMap<DhSectionPos, RenderMetaFile> files = new ConcurrentHashMap<>();
final IClientLevel level;
final File saveDir;
final IDataSourceProvider dataSourceProvider;
public RenderFileHandler(IDataSourceProvider sourceProvider, IClientLevel level, File saveRootDir) {
this.dataSourceProvider = sourceProvider;
this.level = level;
this.saveDir = saveRootDir;
}
/*
* Caller must ensure that this method is called only once,
* and that this object is not used before this method is called.
*/
@Override
public void addScannedFile(Collection<File> detectedFiles) {
HashMultimap<DhSectionPos, RenderMetaFile> filesByPos = HashMultimap.create();
{ // Sort files by pos.
for (File file : detectedFiles) {
try {
RenderMetaFile metaFile = new RenderMetaFile(
dataSourceProvider::isCacheValid,
dataSourceProvider::read,
level, file
);
filesByPos.put(metaFile.pos, metaFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
// Warn for multiple files with the same pos, and then select the one with latest timestamp.
for (DhSectionPos pos : filesByPos.keySet()) {
Collection<RenderMetaFile> metaFiles = filesByPos.get(pos);
RenderMetaFile fileToUse;
if (metaFiles.size() > 1) {
fileToUse = Collections.max(metaFiles, Comparator.comparingLong(a -> a.timestamp));
{
StringBuilder sb = new StringBuilder();
sb.append("Multiple files with the same pos: ");
sb.append(pos);
sb.append("\n");
for (RenderMetaFile metaFile : metaFiles) {
sb.append("\t");
sb.append(metaFile.path);
sb.append("\n");
}
sb.append("\tUsing: ");
sb.append(fileToUse.path);
sb.append("\n");
sb.append("(Other files will be renamed by appending \".old\" to their name.)");
LOGGER.warn(sb.toString());
// Rename all other files with the same pos to .old
for (RenderMetaFile metaFile : metaFiles) {
if (metaFile == fileToUse) continue;
File oldFile = new File(metaFile.path + ".old");
try {
if (!metaFile.path.renameTo(oldFile)) throw new RuntimeException("Renaming failed");
} catch (Exception e) {
LOGGER.error("Failed to rename file: " + metaFile.path + " to " + oldFile, e);
}
}
}
} else {
fileToUse = metaFiles.iterator().next();
}
// Add file to the list of files.
files.put(pos, fileToUse);
}
}
/*
* This call is concurrent. I.e. it supports multiple threads calling this method at the same time.
*/
@Override
public CompletableFuture<LodRenderSource> read(DhSectionPos pos) {
RenderMetaFile metaFile = files.computeIfAbsent(pos, (p) -> new RenderMetaFile(
dataSourceProvider::isCacheValid,
dataSourceProvider::read,
level, computeDefaultFilePath(p), p));
return metaFile.loadOrGetCached(renderCacheThread).handle(
(render, e) -> {
if (e != null) {
LOGGER.error("Uncaught error on {}:", pos, e);
}
if (render != null) return render;
PlaceHolderRenderSource placeHolder = new PlaceHolderRenderSource(pos);
return placeHolder;
}
);
}
/*
* This call is concurrent. I.e. it supports multiple threads calling this method at the same time.
*/
@Override
public void write(DhSectionPos sectionPos, ChunkSizedData chunkData) {
dataSourceProvider.write(sectionPos, chunkData);
recursive_write(sectionPos,chunkData);
}
private void recursive_write(DhSectionPos sectPos, ChunkSizedData chunkData) {
if (!sectPos.getSectionBBoxPos().overlaps(new DhLodPos((byte) (4 + chunkData.dataDetail), chunkData.x, chunkData.z))) return;
if (sectPos.sectionDetail > ColumnRenderSource.SECTION_SIZE_OFFSET) {
recursive_write(sectPos.getChild(0), chunkData);
recursive_write(sectPos.getChild(1), chunkData);
recursive_write(sectPos.getChild(2), chunkData);
recursive_write(sectPos.getChild(3), chunkData);
}
RenderMetaFile metaFile = files.get(sectPos);
if (metaFile != null) { // Fast path: if there is a file for this section, just write to it.
metaFile.updateChunkIfNeeded(chunkData);
}
}
/*
* This call is concurrent. I.e. it supports multiple threads calling this method at the same time.
*/
@Override
public CompletableFuture<Void> flushAndSave() {
ArrayList<CompletableFuture<Void>> futures = new ArrayList<CompletableFuture<Void>>();
for (RenderMetaFile metaFile : files.values()) {
futures.add(metaFile.flushAndSave(renderCacheThread));
}
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
}
private File computeDefaultFilePath(DhSectionPos pos) { //TODO: Temp code as we haven't decided on the file naming & location yet.
return new File(saveDir, pos.serialize() + ".lod");
}
@Override
public void close() {
ArrayList<CompletableFuture<Void>> futures = new ArrayList<CompletableFuture<Void>>();
for (RenderMetaFile metaFile : files.values()) {
futures.add(metaFile.flushAndSave(renderCacheThread));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}
}
@@ -0,0 +1,213 @@
package com.seibel.lod.core.a7.save.io.render;
import com.seibel.lod.core.a7.datatype.LodDataSource;
import com.seibel.lod.core.a7.datatype.LodRenderSource;
import com.seibel.lod.core.a7.datatype.RenderSourceLoader;
import com.seibel.lod.core.a7.datatype.full.ChunkSizedData;
import com.seibel.lod.core.a7.datatype.transform.DataRenderTransformer;
import com.seibel.lod.core.a7.level.IClientLevel;
import com.seibel.lod.core.a7.pos.DhLodPos;
import com.seibel.lod.core.a7.save.io.MetaFile;
import com.seibel.lod.core.a7.pos.DhSectionPos;
import com.seibel.lod.core.util.LodUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
public class RenderMetaFile extends MetaFile {
private final IClientLevel level;
public RenderSourceLoader loader;
public Class<? extends LodRenderSource> dataType;
// The '?' type should either be:
// SoftReference<LodRenderSource>, or - File that may still be loaded
// CompletableFuture<LodRenderSource>,or - File that is being loaded
// null - Nothing is loaded or being loaded
AtomicReference<Object> data = new AtomicReference<>(null);
//FIXME: This can cause concurrent modification of LodRenderSource.
// Not sure if it will cause issues or not.
public void updateChunkIfNeeded(ChunkSizedData chunkData) {
DhLodPos chunkPos = new DhLodPos((byte) (chunkData.dataDetail + 4), chunkData.x, chunkData.z);
LodUtil.assertTrue(pos.getSectionBBoxPos().overlaps(chunkPos), "Chunk pos {} doesn't overlap with section {}", chunkPos, pos);
CompletableFuture<LodRenderSource> source = _readCached(data.get());
if (source == null) return;
if (source.isDone()) source.join().write(chunkData);
}
public CompletableFuture<Void> flushAndSave(ExecutorService renderCacheThread) {
if (!path.exists()) return CompletableFuture.completedFuture(null); // No need to save if the file doesn't exist.
CompletableFuture<LodRenderSource> source = _readCached(data.get());
if (source == null) return CompletableFuture.completedFuture(null); // If there is no cached data, there is no need to save.
return source.thenAccept((a)->{}); // Otherwise, wait for the data to be read (which also flushes changes to the file).
}
@FunctionalInterface
public interface CacheValidator {
boolean isCacheValid(DhSectionPos sectionPos, long timestamp);
}
@FunctionalInterface
public interface CacheSourceProducer {
CompletableFuture<LodDataSource> getSourceFuture(DhSectionPos sectionPos);
}
CacheValidator validator;
CacheSourceProducer source;
// Load a metaFile in this path. It also automatically read the metadata.
public RenderMetaFile(CacheValidator validator, CacheSourceProducer source,
IClientLevel level, File path) throws IOException {
super(path);
this.level = level;
loader = RenderSourceLoader.getLoader(dataTypeId, loaderVersion);
if (loader == null) {
throw new IOException("Invalid file: Data type loader not found: "
+ dataTypeId + "(v" + loaderVersion + ")");
}
dataType = loader.clazz;
this.validator = validator;
this.source = source;
}
// Make a new MetaFile. It doesn't load or write any metadata itself.
public RenderMetaFile(CacheValidator validator, CacheSourceProducer source,
IClientLevel level, File path, DhSectionPos pos) {
super(path, pos);
this.level = level;
this.validator = validator;
this.source = source;
}
// Suppress casting of CompletableFuture<?> to CompletableFuture<LodRenderSource>
@SuppressWarnings("unchecked")
private CompletableFuture<LodRenderSource> _readCached(Object obj) {
// Has file cached in RAM and not freed yet.
if ((obj instanceof SoftReference<?>)) {
Object inner = ((SoftReference<?>)obj).get();
if (inner != null) {
LodUtil.assertTrue(inner instanceof LodRenderSource);
return CompletableFuture.completedFuture((LodRenderSource)inner);
}
}
//==== Cached file out of scope. ====
// Someone is already trying to complete it. so just return the obj.
if ((obj instanceof CompletableFuture<?>)) {
return (CompletableFuture<LodRenderSource>)obj;
}
return null;
}
// Cause: Generic Type runtime casting cannot safety check it.
// However, the Union type ensures the 'data' should only contain the listed type.
public CompletableFuture<LodRenderSource> loadOrGetCached(Executor fileReaderThreads) {
Object obj = data.get();
CompletableFuture<LodRenderSource> cached = _readCached(obj);
if (cached != null) return cached;
// Create an empty and non-completed future.
// Note: I do this before actually filling in the future so that I can ensure only
// one task is submitted to the thread pool.
CompletableFuture<LodRenderSource> future = new CompletableFuture<>();
// Would use faster and non-nesting Compare and exchange. But java 8 doesn't have it! :(
boolean worked = data.compareAndSet(obj, future);
if (!worked) return loadOrGetCached(fileReaderThreads);
// Now, there should only ever be one thread at a time here due to the CAS operation above.
// Would use CompletableFuture.completeAsync(...), But, java 8 doesn't have it! :(
//return future.completeAsync(this::loadAndUpdateRenderSource, fileReaderThreads);
CompletableFuture.supplyAsync(() -> buildFuture(fileReaderThreads), fileReaderThreads)
.thenCompose((sourceCompletableFuture) -> sourceCompletableFuture)
.whenComplete((renderSource, e) -> {
if (e != null) {
LOGGER.error("Uncaught error loading file {}: ", path, e);
future.complete(null);
data.set(null);
} else {
future.complete(renderSource);
data.set(new SoftReference<>(renderSource));
}
});
return future;
}
private CompletableFuture<LodRenderSource> buildFuture(Executor executorService) {
if (path.exists()) {
try {
updateMetaData();
if (validator.isCacheValid(pos, timestamp)) {
// Load the file.
try (FileInputStream fio = getDataContent()) {
return CompletableFuture.completedFuture(
loader.loadRender(this, fio, level));
}
}
} catch (IOException e) {
LOGGER.warn("Failed to read render cache at {}:", path, e);
LOGGER.warn("Will delete cache file.");
path.delete();
}
}
// Otherwise, re-query and make the RenderSource
CompletableFuture<LodDataSource> dataFuture = source.getSourceFuture(pos);
return dataFuture.thenCombineAsync(
DataRenderTransformer.asyncTransformDataSource(dataFuture, level),
this::write, executorService);
}
private FileInputStream getDataContent() throws IOException {
FileInputStream fin = new FileInputStream(path);
int toSkip = METADATA_SIZE;
while (toSkip > 0) {
long skipped = fin.skip(toSkip);
if (skipped == 0) {
throw new IOException("Invalid file: Failed to skip metadata.");
}
toSkip -= skipped;
}
if (toSkip != 0) {
throw new IOException("File IO Error: Failed to skip metadata.");
}
return fin;
}
@Override
protected void updateMetaData() throws IOException {
super.updateMetaData();
loader = RenderSourceLoader.getLoader(dataTypeId, loaderVersion);
if (loader == null) {
throw new IOException("Invalid file: Data type loader not found: " + dataTypeId + "(v" + loaderVersion + ")");
}
dataType = loader.clazz;
dataTypeId = loader.renderTypeId;
}
private LodRenderSource write(LodDataSource parent, LodRenderSource render) {
if (parent == null) return null;
try {
//TODO: Update Timestamp & stuff based on parent
dataLevel = parent.getDataDetail();
loader = RenderSourceLoader.getLoader(render.getClass(), render.getRenderVersion());
dataType = render.getClass();
dataTypeId = loader.renderTypeId;
loaderVersion = render.getRenderVersion();
super.writeData((out) -> {
try {
render.saveRender(level, this, out);
} catch (IOException e) {
LOGGER.error("Failed to save data for file {}", path, e);
}
});
} catch (IOException e) {
LOGGER.error("Failed to write data for file {}", path, e);
}
return render;
}
}
@@ -0,0 +1,167 @@
package com.seibel.lod.core.a7.save.structure;
import com.seibel.lod.core.a7.save.io.LevelToFileMatcher;
import com.seibel.lod.core.config.Config;
import com.seibel.lod.core.enums.config.EServerFolderNameMode;
import com.seibel.lod.core.handlers.dependencyInjection.SingletonInjector;
import com.seibel.lod.core.objects.ParsedIp;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
import javax.annotation.Nullable;
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Optional;
import java.util.stream.Stream;
public class ClientOnlySaveStructure extends SaveStructure {
final File folder;
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
public static final String INVALID_FILE_CHARACTERS_REGEX = "[\\\\/:*?\"<>|]";
private static String getServerFolderName()
{
// parse the current server's IP
ParsedIp parsedIp = new ParsedIp(MC_CLIENT.getCurrentServerIp());
String serverIpCleaned = parsedIp.ip.replaceAll(INVALID_FILE_CHARACTERS_REGEX, "");
String serverPortCleaned = parsedIp.port != null ? parsedIp.port.replaceAll(INVALID_FILE_CHARACTERS_REGEX, "") : "";
// determine the format of the folder name
EServerFolderNameMode folderNameMode = Config.Client.Multiplayer.serverFolderNameMode.get();
if (folderNameMode == EServerFolderNameMode.AUTO)
{
if (parsedIp.isLan())
{
// LAN
folderNameMode = EServerFolderNameMode.NAME_IP;
}
else
{
// normal multiplayer
folderNameMode = EServerFolderNameMode.NAME_IP_PORT;
}
}
String serverName = MC_CLIENT.getCurrentServerName().replaceAll(INVALID_FILE_CHARACTERS_REGEX, "");
String serverMcVersion = MC_CLIENT.getCurrentServerVersion().replaceAll(INVALID_FILE_CHARACTERS_REGEX, "");
// generate the folder name
String folderName = "";
switch (folderNameMode)
{
// default and auto shouldn't be used
// and are just here to make the compiler happy
default:
case NAME_ONLY:
folderName = serverName;
break;
case NAME_IP:
folderName = serverName + ", IP " + serverIpCleaned;
break;
case NAME_IP_PORT:
folderName = serverName + ", IP " + serverIpCleaned + (serverPortCleaned.length() != 0 ? ("-" + serverPortCleaned) : "");
break;
case NAME_IP_PORT_MC_VERSION:
folderName = serverName + ", IP " + serverIpCleaned + (serverPortCleaned.length() != 0 ? ("-" + serverPortCleaned) : "") + ", GameVersion " + serverMcVersion;
break;
}
return folderName;
}
LevelToFileMatcher fileMatcher = null;
final HashMap<ILevelWrapper, File> levelToFileMap = new HashMap<>();
// Fit for Client_Only environment
public ClientOnlySaveStructure() {
folder = new File(MC_CLIENT.getGameDirectory().getPath() +
File.separatorChar + "Distant_Horizons_server_data" + File.separatorChar + getServerFolderName());
if (!folder.exists()) folder.mkdirs(); //TODO: Deal with errors
}
@Override
public File tryGetLevelFolder(ILevelWrapper level) {
return levelToFileMap.computeIfAbsent(level, (l) -> {
if (Config.Client.Multiplayer.multiDimensionRequiredSimilarity.get() == 0) {
if (fileMatcher != null) {
fileMatcher.close();
fileMatcher = null;
}
return getLevelFolderWithoutSimilarityMatching(l);
}
if (fileMatcher == null || !fileMatcher.isFindingLevel(l)) {
LOGGER.info("Loading level for world " + l.getDimensionType().getDimensionName());
fileMatcher = new LevelToFileMatcher(l, folder,
(File[]) getMatchingLevelFolders(l).toArray());
}
File levelFile = fileMatcher.tryGetLevel();
if (levelFile != null) {
fileMatcher.close();
fileMatcher = null;
}
return levelFile;
});
}
private File getLevelFolderWithoutSimilarityMatching(ILevelWrapper level)
{
Stream<File> folders = getMatchingLevelFolders(level);
Optional<File> first = folders.findFirst();
if (first.isPresent())
{
LOGGER.info("Default Sub Dimension set to: [" + LodUtil.shortenString(first.get().getName(), 8) + "...]");
return first.get();
} else { // if no valid sub dimension was found, create a new one
LOGGER.info("Default Sub Dimension not found. Creating: [" + level.getDimensionType().getDimensionName() + "]");
return new File(folder, level.getDimensionType().getDimensionName());
}
}
public Stream<File> getMatchingLevelFolders(@Nullable ILevelWrapper level) {
File[] folders = folder.listFiles();
if (folders==null) return Stream.empty();
return Arrays.stream(folders).filter(
(f) -> {
if (!isValidLevelFolder(f)) return false;
return level==null || f.getName().equalsIgnoreCase(level.getDimensionType().getDimensionName());
}
).sorted();
}
/** Returns true if the given folder holds valid Lod Dimension data */
private static boolean isValidLevelFolder(File potentialFolder)
{
if (!potentialFolder.isDirectory())
// it needs to be a folder
return false;
File[] files = potentialFolder.listFiles((f) -> f.isDirectory() &&
(f.getName().equalsIgnoreCase(RENDER_CACHE_FOLDER) || f.getName().equalsIgnoreCase(DATA_FOLDER)));
// it needs to have folders with specified names in it
return files != null && files.length != 0;
}
@Override
public File getRenderCacheFolder(ILevelWrapper level) {
File levelFolder = levelToFileMap.get(level);
if (levelFolder == null) return null;
return new File(levelFolder, RENDER_CACHE_FOLDER);
}
@Override
public File getDataFolder(ILevelWrapper level) {
File levelFolder = levelToFileMap.get(level);
if (levelFolder == null) return null;
return new File(levelFolder, DATA_FOLDER);
}
@Override
public void close() {
fileMatcher.close();
}
@Override
public String toString() {
return "[ClientOnlySave@"+folder.getName()+"]";
}
}
@@ -0,0 +1,49 @@
package com.seibel.lod.core.a7.save.structure;
import com.seibel.lod.core.handlers.dependencyInjection.SingletonInjector;
import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.IServerLevelWrapper;
import java.io.File;
public class LocalSaveStructure extends SaveStructure {
private static final IMinecraftSharedWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
private File debugPath = new File("");
// Fit for Client_Server & Server_Only environment
public LocalSaveStructure() {
}
@Override
public File tryGetLevelFolder(ILevelWrapper wrapper) {
IServerLevelWrapper serverSide = (IServerLevelWrapper) wrapper;
debugPath = new File(serverSide.getSaveFolder(), "Distant_Horizons");
return new File(serverSide.getSaveFolder(), "Distant_Horizons");
}
@Override
public File getRenderCacheFolder(ILevelWrapper level) {
IServerLevelWrapper serverSide = (IServerLevelWrapper) level;
debugPath = new File(serverSide.getSaveFolder(), "Distant_Horizons");
return new File(new File(serverSide.getSaveFolder(), "Distant_Horizons"), RENDER_CACHE_FOLDER);
}
@Override
public File getDataFolder(ILevelWrapper level) {
IServerLevelWrapper serverSide = (IServerLevelWrapper) level;
debugPath = new File(serverSide.getSaveFolder(), "Distant_Horizons");
return new File(new File(serverSide.getSaveFolder(), "Distant_Horizons"), DATA_FOLDER);
}
@Override
public void close() throws Exception {
}
@Override
public String toString() {
return "[LocalSave at ["+debugPath+"] ]";
}
}
@@ -0,0 +1,21 @@
package com.seibel.lod.core.a7.save.structure;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
import org.apache.logging.log4j.Logger;
import java.io.File;
public abstract class SaveStructure implements AutoCloseable {
public static final String RENDER_CACHE_FOLDER = "cache";
public static final String DATA_FOLDER = "data";
protected static final Logger LOGGER = DhLoggerBuilder.getLogger();
public abstract File tryGetLevelFolder(ILevelWrapper wrapper);
public abstract File getRenderCacheFolder(ILevelWrapper world);
public abstract File getDataFolder(ILevelWrapper world);
}
@@ -0,0 +1,5 @@
package com.seibel.lod.core.a7.util;
public interface CombinableResult<T> {
T combineWith(T b, T c, T d);
}
@@ -0,0 +1,347 @@
package com.seibel.lod.core.a7.util;
import com.seibel.lod.core.a7.pos.DhLodPos;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.util.Atomics;
import com.seibel.lod.core.util.LodUtil;
import org.apache.logging.log4j.Logger;
import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
public class ConcurrentQuadCombinableProviderTree<R extends CombinableResult<R>> {
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public static class Node<R> {
private final DhLodPos pos;
public final AtomicReference<CompletableFuture<R>> future;
// The child node is stored as a weak reference so that it can be garbage collected when that node's future is completed
// and which then releases the hold on that node, thus allowing automatic garbage collection.
public final AtomicReferenceArray<WeakReference<Node<R>>> children = new AtomicReferenceArray<>(4);
@SuppressWarnings("unused")
AtomicReference<Node<R>> parent = null; // This is only used to ensure that the parent is not garbage collected before the child.
private Node(DhLodPos pos, CompletableFuture<R> future) {
this.pos = pos;
this.future = new AtomicReference<>(future);
}
private Node(DhLodPos pos, CompletableFuture<R> future, Node<R> parent) {
this.pos = pos;
this.future = new AtomicReference<>(future);
this.parent = new AtomicReference<>(parent);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Node<R> node = (Node<R>) o;
return pos.equals(node.pos);
}
@Override
public int hashCode() {
return pos.hashCode();
}
public Node<R> setIfNullAndGet(int childIndex, Node<R> newChild) {
WeakReference<Node<R>> newRef = new WeakReference<>(newChild);
WeakReference<Node<R>> oldRef;
do {
oldRef = Atomics.compareAndExchange(children, childIndex, null, newRef);
if (oldRef == null) return newChild; // CompareAndExchange succeeded
Node<R> oldNode = oldRef.get();
if (oldNode != null) return oldNode; // CompareAndExchange failed with old node not null
// Otherwise, the old node weak reference is null.
} while (!children.compareAndSet(childIndex, oldRef, newRef)); // If this cas fails, then try again. (Some other thread beat us to it.)
return newChild; // If we get here, then we successfully replaced the old node weak reference with the new one.
}
}
static class RootMap<R> {
private final ConcurrentHashMap<DhLodPos, WeakReference<Node<R>>> roots = new ConcurrentHashMap<>();
private final int topLevel;
RootMap(int topLevel) {
this.topLevel = topLevel;
}
public int getTopLevel() {
return topLevel;
}
public Node<R> get(DhLodPos pos) {
WeakReference<Node<R>> ref = roots.get(pos);
return ref == null ? null : ref.get();
}
public Node<R> compareNullAndExchange(DhLodPos pos, Node<R> newRoot) {
WeakReference<Node<R>> newRef = new WeakReference<>(newRoot);
WeakReference<Node<R>> oldRef;
do {
oldRef = roots.putIfAbsent(pos, newRef);
if (oldRef == null) return null; // putIfAbsent succeeded
Node<R> oldRoot = oldRef.get();
if (oldRoot != null) return oldRoot; // putIfAbsent failed with old root not null
// Otherwise, the old root weak reference is null.
} while (!roots.replace(pos, oldRef, newRef)); // If this cas fails, then try again. (Some other thread beat us to it.)
return null; // If we get here, then we successfully replaced the old root weak reference with the new one, so return null.
}
public boolean compareNullAndSet(DhLodPos pos, Node<R> newRoot) {
WeakReference<Node<R>> newRef = new WeakReference<>(newRoot);
WeakReference<Node<R>> oldRef;
do {
oldRef = roots.putIfAbsent(pos, newRef);
if (oldRef == null) return true; // putIfAbsent succeeded
Node<R> oldRoot = oldRef.get();
if (oldRoot != null) return false; // putIfAbsent failed with old root not null
// Otherwise, the old root weak reference is null.
} while (!roots.replace(pos, oldRef, newRef)); // If this cas fails, then try again. (Some other thread beat us to it.)
return true; // If we get here, then we successfully replaced the old root weak reference with the new one.
}
public Node<R> setIfNullAndGet(DhLodPos pos, Node<R> newRoot) {
WeakReference<Node<R>> newRef = new WeakReference<>(newRoot);
WeakReference<Node<R>> oldRef;
do {
oldRef = roots.putIfAbsent(pos, newRef);
if (oldRef == null) return newRoot; // putIfAbsent succeeded
Node<R> oldRoot = oldRef.get();
if (oldRoot != null) return oldRoot; // putIfAbsent failed with old root not null
// Otherwise, the old root weak reference is null.
} while (!roots.replace(pos, oldRef, newRef)); // If this cas fails, then try again. (Some other thread beat us to it.)
return newRoot; // If we get here, then we successfully replaced the old root weak reference with the new one.
}
public void clean() {
roots.forEach((k,v) -> {
if (v.get() == null) // Remove the entry if the root is null
roots.remove(k, v); // But only if what we check is what we will be removing. (A CAS operation)
// Otherwise, continue.
// (It is not important that we must remove the entry if the root is null,
// as this is just a cleanup op to shrink the map.)
});
}
}
private final ReentrantReadWriteLock rootMapGlobalLock = new ReentrantReadWriteLock();
private final AtomicReference<RootMap<R>> rootMap = new AtomicReference<>(new RootMap<>(0));
public ConcurrentQuadCombinableProviderTree() {}
@Override
public String toString() {
return "CQCPT@" + rootMap.get().topLevel + "(~" + rootMap.get().roots.size() + ")";
}
// Atomically update and get the generation future
private CompletableFuture<R> checkAndMakeFuture(Node<R> node, Function<DhLodPos, CompletableFuture<R>> allNullCompleter) {
CompletableFuture<R> future = new CompletableFuture<>();
CompletableFuture<R> casValue = Atomics.compareAndExchange(node.future, null, future);
if (casValue != null) { // cas failed. Existing future. Return it.
return casValue;
}
// Next, we need to make the future completable.
// We first check for each child connection if it exists. If it does, we store it for a later 'allOf'.
boolean allNull = true;
@SuppressWarnings("unchecked")
CompletableFuture<R>[] childFutures = new CompletableFuture[4];
for (int i = 0; i < 4; i++) {
WeakReference<Node<R>> childRef = node.children.get(i);
Node<R> nextChild = childRef == null ? null : childRef.get();
if (nextChild != null) { // child node exists. Recursively make or get the child's future.
allNull = false;
childFutures[i] = checkAndMakeFuture(nextChild, allNullCompleter);
}
}
if (allNull) { // all children are null. We can then just run the allNullCompleter in this node.
allNullCompleter.apply(node.pos).whenComplete((r, e) -> {
// NOTE(*1): This *HAVE* to get the future via the node reference instead of directly capturing the future,
// as otherwise the node will be garbage collected before the future is completed.
// With this, we can guarantee that the node is garbage collected only when the future is (being) completed.
// (The actual order is not important however as long as the node is still alive when the generation is in progress)
CompletableFuture<R> f = node.future.get();
LodUtil.assertTrue(f != null, "Future should not be null");
if (e != null) {
f.completeExceptionally(e);
} else {
f.complete(r);
}
});
} else { // some children exist. We need to wait for some or all of them to complete.
// But before that, we need to create the children node where they are missing.
for (int i = 0; i < 4; i++) {
if (childFutures[i] == null) {
CompletableFuture<R> newChildFuture = new CompletableFuture<>();
Node<R> newChild = new Node<>(node.pos.getChild(i), newChildFuture, node);
node.children.set(i, new WeakReference<>(newChild));
childFutures[i] = newChildFuture;
// Since the child is new, we can be sure that it doesn't have any children.
// So, we need to make the new child's future completable by running the allNullCompleter.
// (The above relies on the fact that we did a CAS on the beginning of this method,
// which means that we have unique access to the node and its links to the children, and that
// no other thread can be concurrently modifying its links)
allNullCompleter.apply(newChild.pos).whenComplete((r, e) -> {
// NOTE: Same as 'NOTE(*1)', we *HAVE* to get the future via the node reference instead of directly capturing the future.
CompletableFuture<R> f = newChild.future.get();
LodUtil.assertTrue(f != null, "Future should not be null");
if (e != null) {
f.completeExceptionally(e);
} else {
f.complete(r);
}
});
}
LodUtil.assertTrue(childFutures[i] != null);
}
// Now, we can wait for all the child futures to complete, and then complete this node's future with
// the combined result of all child futures.
CompletableFuture.allOf(childFutures).handle((v, e) -> {
// NOTE: Same as 'NOTE(*1)', we *HAVE* to get the future via the node reference instead of directly capturing the future.
CompletableFuture<R> f = node.future.get();
LodUtil.assertTrue(f != null, "Future should not be null");
if (e != null) {
f.completeExceptionally(e);
} else {
try {
f.complete(childFutures[0].join().combineWith(
childFutures[1].join(), childFutures[2].join(), childFutures[3].join()));
} catch (Throwable e2) {
f.completeExceptionally(e2);
}
}
return null;
});
}
return future;
}
public CompletableFuture<R> createOrUseExisting(DhLodPos pos, Function<DhLodPos, CompletableFuture<R>> completer) {
LOGGER.info("Creating or using existing future for {}", pos);
int cleanRng = ThreadLocalRandom.current().nextInt(0, 10);
if (cleanRng == 0) cleanIfNeeded();
// First, ensure that the root map is locked for reading. (The lock is for the structure of the map, not the values)
rootMapGlobalLock.readLock().lock();
RootMap<R> map = rootMap.get();
// Next, do different thing depending on the top level of the map compared to the target position.
if (map.topLevel == pos.detail) { // The target position is at the top level, meaning that we can directly use the root.
// Make the future and node first for the later CAS on null.
CompletableFuture<R> future = new CompletableFuture<>();
Node<R> newNode = new Node<R>(pos, future); // No parent node as it's the root.
Node<R> cas = map.compareNullAndExchange(pos, newNode); // CAS the node into the map.
rootMapGlobalLock.readLock().unlock(); // We're done with the map, as following code no longer accesses it.
if (cas == null) { // cas succeeded. Which means no existing overlapping node in same detail level.
// Reason: Since any lower level nodes should have upper level nodes as parent up to the top level,
// and that there are no same level nodes, we can assume that the new node does not overlap any existing nodes.
// Therefore, we can apply the completer function to the new node, and return the future.
completer.apply(pos).whenComplete((r, e) -> {
// See NOTE(*1) above.
CompletableFuture<R> f = newNode.future.get();
LodUtil.assertTrue(f != null, "Future should not be null");
if (e != null) {
f.completeExceptionally(e);
} else {
f.complete(r);
}
});
return future;
} else { // cas failed. Existing overlapping node.
// Run the checkAndMakeFuture method on the existing node to update and get the generation future.
return checkAndMakeFuture(cas, completer);
}
} else if (map.topLevel > pos.detail) {
// We need to traverse down the tree with the following rules during the traversal:
// 1. If the next node is not null and has a future, halt and return that future.
// 2. If the next node is not null with no future, continue traversing down the tree.
// 3. if the next node is null, create a new node and CompareExchange it into the current node, and run rule 1/2.
// Note that DO NOT assume that all subsequent nodes will fall into case 3, as someone else can concurrently
// use and modify the newly created node!
// To start, just treat the rootMap as the... well, root, and it's content as the children node.
// We can then traverse down the tree until we reach the target node or hit the 1st case and return prematurely.
// First iteration:
Node<R> currentNode;
DhLodPos childPos = pos.convertUpwardsTo((byte) map.topLevel);
Node<R> childNode = map.setIfNullAndGet( // rule 3: if null, create a new node.
childPos, new Node<R>(childPos, null)); // No parent node as it's the root.
rootMapGlobalLock.readLock().unlock(); // We're done with the map, as following code no longer accesses it.
CompletableFuture<R> future = childNode.future.get();
if (future != null) { // rule 1: if future is not null, halt and return the future.
return future;
} else { // rule 2: if future is null, continue traversing down the tree.
currentNode = childNode;
// Second and subsequent iterations:
while (currentNode.pos.detail > pos.detail) {
childPos = pos.convertUpwardsTo((byte) (currentNode.pos.detail - 1));
// Note: It is important that child link is set and created before we check the child future,
// so to avoid race conditions with checkAndMakeFuture.
childNode = currentNode.setIfNullAndGet(childPos.getChildIndexOfParent(),
new Node<R>(childPos, null, currentNode)); // rule 3: if null, create a new node.
CompletableFuture<R> childFuture = childNode.future.get();
if (childFuture != null) { // rule 1: if future is not null, halt and return the future.
return childFuture;
} else { // rule 2: if future is null, continue traversing down the tree.
currentNode = childNode;
}
}
}
// At this point, we have reached the target node.
LodUtil.assertTrue(currentNode.pos.equals(pos));
// We can now run the checkAndMakeFuture method on the target node to update and get the generation future.
return checkAndMakeFuture(currentNode, completer); // Technically, this will rerun the 1st rule. But code is cleaner this way.
} else { // map.topLevel < pos.detail
// Now, this is the complex case. We need to rebase the tree to the higher detail level.
// For now, this implementation will do a lock based version. However, I will figure out a way to do this without a lock.
rootMapGlobalLock.readLock().unlock();
while (map.topLevel < pos.detail) {
map = rebaseUpward(pos.detail);
}
LodUtil.assertTrue(map.topLevel >= pos.detail);
return createOrUseExisting(pos, completer); // After rebasing, we can just call the createOrUseExisting method again.
}
}
private RootMap<R> rebaseUpward(int targetLevel) {
rootMapGlobalLock.writeLock().lock();
try {
RootMap<R> map = rootMap.get();
if (map.topLevel >= targetLevel) {
return map;
}
// At this point, we have exclusive access to the rootMap.
map.clean(); // Clean the map. (Could actually be done with just readLock.)
RootMap<R> newMap = new RootMap<>(map.topLevel + 1);
map.roots.forEach((pos, nodeRef) -> {
Node<R> node = nodeRef.get();
if (node == null) return; // If null, ignore that node.
LodUtil.assertTrue(pos.detail+1 == newMap.topLevel);
LodUtil.assertTrue(node.parent.get() == null);
LodUtil.assertTrue(node.pos.equals(pos));
DhLodPos newPos = pos.convertUpwardsTo((byte) (pos.detail+1));
// Create the parent node, or if it already exists, use it to set the child node's parent.
// NOTE: While this section is protected by the rootMapGlobalLock, we still need to use the normal
// CAS methods to setAndGet the parent node, as the parent node may be GC'd concurrently by other threads
// who have just completed the node's future, and caused the GC parent chain up to the new map.
Node<R> newParentNode = newMap.setIfNullAndGet(newPos, new Node<R>(newPos, null));
node.parent.set(newParentNode);
});
boolean casWorked = rootMap.compareAndSet(map, newMap);
LodUtil.assertTrue(casWorked);
return newMap;
} finally {
rootMapGlobalLock.writeLock().unlock();
}
}
public void cleanIfNeeded() {
if (rootMapGlobalLock.readLock().tryLock()) {
rootMap.get().clean();
rootMapGlobalLock.readLock().unlock();
}
}
}
@@ -0,0 +1,45 @@
package com.seibel.lod.core.a7.util;
import com.seibel.lod.core.a7.save.io.file.IDataSourceProvider;
import com.seibel.lod.core.a7.save.io.render.IRenderSourceProvider;
import com.seibel.lod.core.a7.save.structure.SaveStructure;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
import org.apache.logging.log4j.Logger;
import javax.annotation.Nullable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Collectors;
import java.util.stream.Stream;
// Static util class??
public class FileScanner {
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public static final int MAX_SCAN_DEPTH = 5;
public static final String LOD_FILE_POSTFIX = ".lod";
public static void scanFile(SaveStructure save, ILevelWrapper level,
@Nullable IDataSourceProvider dataSource,
@Nullable IRenderSourceProvider renderSource) {
if (dataSource != null) {
try (Stream<Path> pathStream = Files.walk(save.getDataFolder(level).toPath(), MAX_SCAN_DEPTH)) {
dataSource.addScannedFile(pathStream.filter(
path -> path.toFile().getName().endsWith(LOD_FILE_POSTFIX) && path.toFile().isFile()
).map(Path::toFile).collect(Collectors.toList())
);
} catch (Exception e) {
LOGGER.error("Failed to scan and collect data files for {} in {}", level, save, e);
}
}
if (renderSource != null) {
try (Stream<Path> pathStream = Files.walk(save.getRenderCacheFolder(level).toPath(), MAX_SCAN_DEPTH)) {
renderSource.addScannedFile(pathStream.filter((
path -> path.toFile().getName().endsWith(LOD_FILE_POSTFIX) && path.toFile().isFile())
).map(Path::toFile).collect(Collectors.toList())
);
} catch (Exception e) {
LOGGER.error("Failed to scan and collect data files for {} in {}", level, save, e);
}
}
}
}
@@ -0,0 +1,6 @@
package com.seibel.lod.core.a7.util;
public class IOUtil {
public static final String LOD_FILE_EXTENSION = ".lod";
}
@@ -0,0 +1,6 @@
package com.seibel.lod.core.a7.util;
public class IdMappingUtil {
public static final String BLOCKSTATE_ID_AIR = "air";
//TODO HERE
}
@@ -0,0 +1,514 @@
package com.seibel.lod.core.a7.util;
import com.seibel.lod.core.a7.pos.DhLodPos;
import com.seibel.lod.core.util.LodUtil;
import org.apache.commons.lang3.NotImplementedException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
public class LazySectionPosTree<T> implements ConcurrentMap<DhLodPos, T> {
class Node implements Entry<DhLodPos, T> {
private Node parent;
private int child0to3;
private final DhLodPos pos;
private final AtomicInteger sizeCounter = size;
private T value = null;
private Node child0 = null;
private Node child1 = null;
private Node child2 = null;
private Node child3 = null;
private Node(Node parent, int child0to3, DhLodPos pos) {
this.parent = parent;
this.child0to3 = child0to3;
this.pos = pos;
}
private Node(Node parent, int child0to3, DhLodPos pos, T value) {
this.parent = parent;
this.child0to3 = child0to3;
this.pos = pos;
this.value = value;
}
@Override
public DhLodPos getKey() {
return pos;
}
@Override
public T getValue() {
return value;
}
@Override
public T setValue(T value) {
T old = this.value;
this.value = value;
if (old == null && value != null) {
sizeCounter.incrementAndGet();
} else if (old != null && value == null) {
sizeCounter.decrementAndGet();
}
return old;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Node node = (Node) o;
return pos.equals(node.pos);
}
@Override
public int hashCode() {
return pos.hashCode();
}
private T setIfAbsent(T value) {
T old = this.value;
if (old == null) {
this.value = value;
sizeCounter.incrementAndGet();
}
return old;
}
private T computeIfAbsent(@NotNull Function<? super DhLodPos, ? extends T> mappingFunction) {
if (value == null) {
value = mappingFunction.apply(pos);
sizeCounter.incrementAndGet();
}
return value;
}
private T computeIfPresent(@NotNull BiFunction<? super DhLodPos, ? super T, ? extends T> remappingFunction) {
if (value != null) {
T newValue = remappingFunction.apply(pos, value);
if (newValue != null) {
value = newValue;
} else {
sizeCounter.decrementAndGet();
value = null;
}
}
return value;
}
private boolean noChildren() {
return child0 == null && child1 == null && child2 == null && child3 == null;
}
private Node makeOrGetChild(int child0to3) {
LodUtil.assertTrue(child0to3 >= 0 && child0to3 <= 3);
switch (child0to3) {
case 0:
return child0 == null ? child0 = new Node(this, 0, pos.getChild(0)) : child0;
case 1:
return child1 == null ? child1 = new Node(this, 1, pos.getChild(1)) : child1;
case 2:
return child2 == null ? child2 = new Node(this, 2, pos.getChild(2)) : child2;
case 3:
return child3 == null ? child3 = new Node(this, 3, pos.getChild(3)) : child3;
}
LodUtil.assertNotReach();
return new Node(null, 0, pos.getChild(0)); // unreachable. Just hack to make contract happy.
}
private Node getChild(int child0to3) {
LodUtil.assertTrue(child0to3 >= 0 && child0to3 <= 3);
switch (child0to3) {
case 0:
return child0;
case 1:
return child1;
case 2:
return child2;
case 3:
return child3;
}
LodUtil.assertNotReach();
return null;
}
private void removeChild(int child0to3) {
LodUtil.assertTrue(child0to3 >= 0 && child0to3 <= 3);
switch (child0to3) {
case 0:
child0 = null;
break;
case 1:
child1 = null;
break;
case 2:
child2 = null;
break;
case 3:
child3 = null;
break;
}
LodUtil.assertNotReach();
}
private void setChild(int child0to3, Node child) {
LodUtil.assertTrue(child0to3 >= 0 && child0to3 <= 3);
child.parent = this;
switch (child0to3) {
case 0:
child0 = child;
child.child0to3 = 0;
break;
case 1:
child1 = child;
child.child0to3 = 1;
break;
case 2:
child2 = child;
child.child0to3 = 2;
break;
case 3:
child3 = child;
child.child0to3 = 3;
break;
}
LodUtil.assertNotReach();
}
}
private ConcurrentSkipListMap<DhLodPos, Node> nodes = new ConcurrentSkipListMap<>();
private byte topLevel = 0;
private AtomicInteger size = new AtomicInteger(0);
public LazySectionPosTree() {}
@Override
public int hashCode() {
throw new NotImplementedException();
}
@Override
public boolean equals(Object obj) {
throw new NotImplementedException();
}
@Override
protected Object clone() throws CloneNotSupportedException {
throw new NotImplementedException();
}
@Override
public String toString() {
throw new NotImplementedException();
}
@Override
public int size() {
return size.get();
}
@Override
public boolean isEmpty() {
return size.get() == 0;
}
private Node travel(Node from, DhLodPos pos) {
if (from == null) return null;
LodUtil.assertTrue(pos != null);
LodUtil.assertTrue(from.pos.detail > pos.detail);
LodUtil.assertTrue(from.pos.overlaps(pos));
byte iterDetail = from.pos.detail;
while (iterDetail > pos.detail) {
from = from.getChild(pos.convertUpwardsTo(--iterDetail).getChildIndexOfParent());
if (from == null) return null;
}
LodUtil.assertTrue(from.pos.equals(pos));
return from;
}
private Node initTravel(Node from, DhLodPos pos) {
LodUtil.assertTrue(from != null);
LodUtil.assertTrue(pos != null);
LodUtil.assertTrue(from.pos.detail > pos.detail);
LodUtil.assertTrue(from.pos.overlaps(pos));
byte iterDetail = from.pos.detail;
while (iterDetail > pos.detail)
from = from.makeOrGetChild(pos.convertUpwardsTo(--iterDetail).getChildIndexOfParent());
LodUtil.assertTrue(from.pos.equals(pos));
return from;
}
private void upcastTreeBase() {
}
private byte upcastSingeTreeBase() {
byte nextLevel = (byte) (topLevel + 1);
ConcurrentSkipListMap<DhLodPos, Node> newBase = new ConcurrentSkipListMap<>();
nodes.forEach((pos, node) ->
newBase.compute(pos.convertUpwardsTo(nextLevel), (key, old) -> {
if (old == null) {
old = new Node(null, 0, pos.convertUpwardsTo(nextLevel));
}
old.setChild(pos.getChildIndexOfParent(), node);
return old;
})
);
nodes = newBase; // todo: cas operation to here. (Will be block free but not wait free)
topLevel = nextLevel; //todo: atomic???
return nextLevel;
}
private void downcastTreeBase() {
byte prevLevel = (byte) (topLevel - 1);
ConcurrentSkipListMap<DhLodPos, Node> newBase = new ConcurrentSkipListMap<>();
}
@Override
public boolean containsKey(Object key) {
DhLodPos pos = (DhLodPos) key;
if (pos.detail > topLevel) return false;
if (pos.detail == topLevel) return nodes.containsKey(pos);
Node node = travel(nodes.get(pos.convertUpwardsTo(topLevel)), pos);
return node != null;
}
@Override
public boolean containsValue(Object value) {
throw new UnsupportedOperationException("Such operation is not supported in LazySectionPosTree");
}
@Override
public T get(Object key) {
DhLodPos pos = (DhLodPos) key;
if (pos.detail > topLevel) return null;
if (pos.detail == topLevel) return nodes.get(pos).value;
Node node = travel(nodes.get(pos.convertUpwardsTo(topLevel)), pos);
return node == null ? null : node.value;
}
@Override
public T getOrDefault(Object key, T defaultValue) {
T value = get(key);
return value == null ? defaultValue : value;
}
@Nullable
@Override
public T put(DhLodPos key, T value) {
if (key.detail == topLevel) {
return nodes.computeIfAbsent(key, k -> new Node(null, 0, key, value)).setValue(value);
}
if (key.detail < topLevel) {
Node node = initTravel(nodes.get(key.convertUpwardsTo(topLevel)), key);
return node.setValue(value);
}
// key.detail > topLevel:
// Rebase the tree
//upcastTreeBase(key.detail);
return nodes.computeIfAbsent(key, k -> new Node(null, 0, key, value)).setValue(value);
}
private void removeNode(Node node) {
if (node.parent != null) {
node.parent.removeChild(node.child0to3);
if (node.parent.noChildren()) {
removeNode(node.parent);
}
}
else nodes.remove(node.pos);
}
@Override
public T remove(Object key) {
DhLodPos pos = (DhLodPos) key;
if (pos.detail > topLevel) return null;
Node node;
if (pos.detail == topLevel) {
node = nodes.remove(pos);
} else {
node = travel(nodes.get(pos.convertUpwardsTo(topLevel)), pos);
}
if (node == null) return null;
// Pop the value
T value = node.setValue(null);
// Delete the node if there are no children
if (node.noChildren()) {
removeNode(node);
}
return value;
}
@Override
public boolean remove(@NotNull Object key, Object value) {
DhLodPos pos = (DhLodPos) key;
if (pos.detail > topLevel) return false;
Node node;
if (pos.detail == topLevel) {
node = nodes.get(pos);
} else {
node = travel(nodes.get(pos.convertUpwardsTo(topLevel)), pos);
}
if (node == null) return false;
//TODO: Make this atomic
if (node.value.equals(value)) {
removeNode(node);
return true;
}
return false;
}
@Override
public boolean replace(@NotNull DhLodPos key, @NotNull T oldValue, @NotNull T newValue) {
if (key.detail > topLevel) return false;
Node node;
if (key.detail == topLevel) {
node = nodes.get(key);
} else {
node = travel(nodes.get(key.convertUpwardsTo(topLevel)), key);
}
if (node == null) return false;
//TODO: Make this atomic
if (node.value.equals(oldValue)) {
node.setValue(newValue);
return true;
}
return false;
}
@Override
public T replace(@NotNull DhLodPos key, @NotNull T value) {
if (key.detail == topLevel) {
Node n = nodes.get(key);
//TODO: Make this atomic
if (n == null || n.value==null) return null;
return n.setValue(value);
}
if (key.detail < topLevel) {
Node node = travel(nodes.get(key.convertUpwardsTo(topLevel)), key);
//TODO: Make this atomic
if (node == null || node.value==null) return null;
return node.setValue(value);
}
// key.detail > topLevel: Does not exist
return null;
}
@Nullable
@Override
public T putIfAbsent(@NotNull DhLodPos key, T value) {
if (key.detail == topLevel) {
return nodes.computeIfAbsent(key, k -> new Node(null, 0, key, null)).setIfAbsent(value);
}
if (key.detail < topLevel) {
Node node = initTravel(nodes.get(key.convertUpwardsTo(topLevel)), key);
return node.setIfAbsent(value);
}
// key.detail > topLevel:
// Rebase the tree
//upcastTreeBase(key.detail);
return nodes.computeIfAbsent(key, k -> new Node(null, 0, key, null)).setIfAbsent(value);
}
@Override
public T computeIfAbsent(DhLodPos key, @NotNull Function<? super DhLodPos, ? extends T> mappingFunction) {
if (key.detail == topLevel) {
return nodes.computeIfAbsent(key, k -> new Node(null, 0, key, null)).computeIfAbsent(mappingFunction);
}
if (key.detail < topLevel) {
Node node = initTravel(nodes.get(key.convertUpwardsTo(topLevel)), key);
return node.computeIfAbsent(mappingFunction);
}
// key.detail > topLevel:
// Rebase the tree
//upcastTreeBase(key.detail);
return nodes.computeIfAbsent(key, k -> new Node(null, 0, key, null)).computeIfAbsent(mappingFunction);
}
@Override
public T computeIfPresent(DhLodPos key, @NotNull BiFunction<? super DhLodPos, ? super T, ? extends T> remappingFunction) {
if (key.detail == topLevel) {
Node n = nodes.get(key);
if (n == null) return null;
T r = n.computeIfPresent(remappingFunction);
if (r == null && n.noChildren()) {
nodes.remove(key);
}
return r;
}
if (key.detail < topLevel) {
Node node = travel(nodes.get(key.convertUpwardsTo(topLevel)), key);
if (node == null) return null;
T r = node.computeIfPresent(remappingFunction);
if (r == null && node.noChildren()) {
removeNode(node);
}
}
// key.detail > topLevel: Does not exist
return null;
}
// TODO: Improve this naive implementation of compute
@Override
public T compute(DhLodPos key, @NotNull BiFunction<? super DhLodPos, ? super T, ? extends T> remappingFunction) {
T r = get(key);
if (r == null) {
r = remappingFunction.apply(key, null);
if (r != null) {
put(key, r);
}
} else {
r = remappingFunction.apply(key, r);
if (r != null) {
put(key, r);
} else {
remove(key);
}
}
return r;
}
// TODO: Optimize putAll
@Override
public void putAll(@NotNull Map<? extends DhLodPos, ? extends T> m) {
for (Map.Entry<? extends DhLodPos, ? extends T> entry : m.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
@Override
public void clear() {
nodes.clear();
size = new AtomicInteger(0); // Do this to swap the counter obj so old nodes won't mess up the counter
}
@NotNull
@Override
public Set<DhLodPos> keySet() {
//TODO
throw new NotImplementedException();
}
@NotNull
@Override
public Collection<T> values() {
//TODO
throw new NotImplementedException();
}
@NotNull
@Override
public Set<Entry<DhLodPos, T>> entrySet() {
//TODO
throw new NotImplementedException();
}
@Override
public void forEach(BiConsumer<? super DhLodPos, ? super T> action) {
//TODO
throw new NotImplementedException();
}
@Override
public void replaceAll(BiFunction<? super DhLodPos, ? super T, ? extends T> function) {
//TODO
throw new NotImplementedException();
}
// merge: Use default implementation
//public T merge(DhLodPos key, @NotNull T value, @NotNull BiFunction<? super T, ? super T, ? extends T> remappingFunction);
}
@@ -0,0 +1,40 @@
package com.seibel.lod.core.a7.util;
public class UncheckedInterruptedException extends RuntimeException {
public UncheckedInterruptedException(String message) {
super(message);
}
public UncheckedInterruptedException(Throwable cause) {
super(cause);
}
public UncheckedInterruptedException(String message, Throwable cause) {
super(message, cause);
}
public UncheckedInterruptedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public UncheckedInterruptedException() {
super();
}
public static void throwIfInterrupted() {
if (Thread.currentThread().isInterrupted()) {
throw new UncheckedInterruptedException();
}
}
public static UncheckedInterruptedException convert(InterruptedException e) {
return new UncheckedInterruptedException(e);
}
public static void rethrowIfIsInterruption(Throwable t) {
if (t instanceof InterruptedException) {
throw convert((InterruptedException) t);
} else if (t instanceof UncheckedInterruptedException) {
throw (UncheckedInterruptedException) t;
}
}
public static boolean isThrowableInterruption(Throwable t) {
return t instanceof InterruptedException || t instanceof UncheckedInterruptedException;
}
}
@@ -0,0 +1,16 @@
package com.seibel.lod.core.a7.util;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
public class UnclosableInputStream extends FilterInputStream {
public UnclosableInputStream(InputStream it) {
super(it);
}
@Override
public void close() throws IOException {
// Do nothing.
}
}
@@ -0,0 +1,13 @@
package com.seibel.lod.core.a7.util;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class UnclosableOutputStream extends FilterOutputStream {
public UnclosableOutputStream(OutputStream it) {
super(it);
}
@Override
public void close() throws IOException {}
}
@@ -0,0 +1,130 @@
package com.seibel.lod.core.a7.world;
import com.seibel.lod.core.a7.level.DhClientServerLevel;
import com.seibel.lod.core.a7.level.ILevel;
import com.seibel.lod.core.a7.save.structure.LocalSaveStructure;
import com.seibel.lod.core.config.Config;
import com.seibel.lod.core.util.EventLoop;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.IServerLevelWrapper;
import java.io.File;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
public class DhClientServerWorld extends DhWorld implements IClientWorld, IServerWorld
{
private final HashMap<ILevelWrapper, DhClientServerLevel> levels;
public final LocalSaveStructure saveStructure;
public ExecutorService dhTickerThread = LodUtil.makeSingleThreadPool("DHTickerThread", 2);
public EventLoop eventLoop = new EventLoop(dhTickerThread, this::_clientTick); //TODO: Rate-limit the loop
public DhClientServerWorld() {
super(WorldEnvironment.Client_Server);
saveStructure = new LocalSaveStructure();
levels = new HashMap<>();
LOGGER.info("Started DhWorld of type {}", environment);
}
@Override
public DhClientServerLevel getOrLoadLevel(ILevelWrapper wrapper) {
if (wrapper instanceof IServerLevelWrapper) {
return levels.computeIfAbsent(wrapper, (w) -> {
File levelFile = saveStructure.tryGetLevelFolder(w);
LodUtil.assertTrue(levelFile != null);
return new DhClientServerLevel(saveStructure, (IServerLevelWrapper) w);
});
} else {
return levels.computeIfAbsent(wrapper, (w) -> {
IClientLevelWrapper clientSide = (IClientLevelWrapper) w;
IServerLevelWrapper serverSide = clientSide.tryGetServerSideWrapper();
LodUtil.assertTrue(serverSide != null);
DhClientServerLevel level = levels.get(serverSide);
if (level==null) return null;
level.startRenderer(clientSide);
return level;
});
}
}
@Override
public DhClientServerLevel getLevel(ILevelWrapper wrapper) {
return levels.get(wrapper);
}
@Override
public ILevel[] getAllLoadedLevels()
{
ILevel[] array = new ILevel[this.levels.size()];
int i = 0;
for (ILevel level : this.levels.values())
{
array[i] = level;
i++;
}
return array;
}
@Override
public void unloadLevel(ILevelWrapper wrapper) {
if (levels.containsKey(wrapper)) {
if (wrapper instanceof IServerLevelWrapper) {
LOGGER.info("Unloading level {} ", levels.get(wrapper));
levels.remove(wrapper).close();
} else {
levels.remove(wrapper).stopRenderer(); // Ignore resource warning. The level obj is referenced elsewhere.
}
}
}
private void _clientTick() {
//LOGGER.info("Client world tick with {} levels", levels.size());
int newViewDistance = Config.Client.Graphics.Quality.lodChunkRenderDistance.get() * 16;
Iterator<DhClientServerLevel> iterator = levels.values().iterator();
while (iterator.hasNext()) {
DhClientServerLevel level = iterator.next();
if (level.tree != null && level.tree.viewDistance != newViewDistance) {
level.close(); //FIXME: Is this fine for current logic?
iterator.remove();
}
}
//DetailDistanceUtil.updateSettings();
levels.values().forEach(DhClientServerLevel::clientTick);
}
public void clientTick() {
//LOGGER.info("Client world tick");
eventLoop.tick();
}
public void serverTick() {
levels.values().forEach(DhClientServerLevel::serverTick);
}
public void doWorldGen() {
levels.values().forEach(DhClientServerLevel::doWorldGen);
}
@Override
public CompletableFuture<Void> saveAndFlush() {
return CompletableFuture.allOf(levels.values().stream().map(DhClientServerLevel::save).toArray(CompletableFuture[]::new));
}
@Override
public void close() {
saveAndFlush().join();
for (DhClientServerLevel level : levels.values()) {
LOGGER.info("Unloading level " + level.serverLevel.getDimensionType().getDimensionName());
level.close();
}
levels.clear();
LOGGER.info("Closed DhWorld of type {}", environment);
}
}
@@ -0,0 +1,108 @@
package com.seibel.lod.core.a7.world;
import com.seibel.lod.core.a7.level.DhClientLevel;
import com.seibel.lod.core.a7.level.DhClientServerLevel;
import com.seibel.lod.core.a7.level.ILevel;
import com.seibel.lod.core.a7.save.structure.ClientOnlySaveStructure;
import com.seibel.lod.core.config.Config;
import com.seibel.lod.core.util.DetailDistanceUtil;
import com.seibel.lod.core.util.EventLoop;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
import java.io.File;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
public class DhClientWorld extends DhWorld implements IClientWorld
{
private final HashMap<IClientLevelWrapper, DhClientLevel> levels;
public final ClientOnlySaveStructure saveStructure;
public ExecutorService dhTickerThread = LodUtil.makeSingleThreadPool("DHTickerThread", 2);
public EventLoop eventLoop = new EventLoop(dhTickerThread, this::_clientTick);
public DhClientWorld() {
super(WorldEnvironment.Client_Only);
saveStructure = new ClientOnlySaveStructure();
levels = new HashMap<>();
LOGGER.info("Started DhWorld of type {}", environment);
}
@Override
public DhClientLevel getOrLoadLevel(ILevelWrapper wrapper) {
if (!(wrapper instanceof IClientLevelWrapper)) return null;
return levels.computeIfAbsent((IClientLevelWrapper) wrapper, (w) -> {
File level = saveStructure.tryGetLevelFolder(wrapper);
if (level == null) return null;
return new DhClientLevel(saveStructure, w);
});
}
@Override
public DhClientLevel getLevel(ILevelWrapper wrapper) {
if (!(wrapper instanceof IClientLevelWrapper)) return null;
return levels.get(wrapper);
}
@Override
public ILevel[] getAllLoadedLevels()
{
ILevel[] array = new ILevel[this.levels.size()];
int i = 0;
for (ILevel level : this.levels.values())
{
array[i] = level;
i++;
}
return array;
}
@Override
public void unloadLevel(ILevelWrapper wrapper) {
if (!(wrapper instanceof IClientLevelWrapper)) return;
if (levels.containsKey(wrapper)) {
LOGGER.info("Unloading level {} ", levels.get(wrapper));
levels.remove(wrapper).close();
}
}
private void _clientTick() {
int newViewDistance = Config.Client.Graphics.Quality.lodChunkRenderDistance.get() * 16;
Iterator<DhClientLevel> iterator = levels.values().iterator();
while (iterator.hasNext()) {
DhClientLevel level = iterator.next();
if (level.tree.viewDistance != newViewDistance) {
level.close();
iterator.remove();
}
}
DetailDistanceUtil.updateSettings();
levels.values().forEach(DhClientLevel::clientTick);
}
public void clientTick() {
eventLoop.tick();
}
@Override
public CompletableFuture<Void> saveAndFlush() {
return CompletableFuture.allOf(levels.values().stream().map(DhClientLevel::save).toArray(CompletableFuture[]::new));
}
@Override
public void close() {
saveAndFlush().join();
for (DhClientLevel level : levels.values()) {
LOGGER.info("Unloading level " + level.level.getDimensionType().getDimensionName());
level.close();
}
levels.clear();
LOGGER.info("Closed DhWorld of type {}", environment);
}
}
@@ -0,0 +1,92 @@
package com.seibel.lod.core.a7.world;
import com.seibel.lod.core.a7.level.DhServerLevel;
import com.seibel.lod.core.a7.level.ILevel;
import com.seibel.lod.core.a7.save.structure.LocalSaveStructure;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.IServerLevelWrapper;
import java.io.File;
import java.util.HashMap;
import java.util.concurrent.CompletableFuture;
public class DhServerWorld extends DhWorld implements IServerWorld
{
private final HashMap<IServerLevelWrapper, DhServerLevel> levels;
public final LocalSaveStructure saveStructure;
public DhServerWorld() {
super(WorldEnvironment.Server_Only);
saveStructure = new LocalSaveStructure();
levels = new HashMap<>();
LOGGER.info("Started DhWorld of type {}", environment);
}
@Override
public DhServerLevel getOrLoadLevel(ILevelWrapper wrapper) {
if (!(wrapper instanceof IServerLevelWrapper)) return null;
return levels.computeIfAbsent((IServerLevelWrapper) wrapper, (w) -> {
File levelFile = saveStructure.tryGetLevelFolder(wrapper);
LodUtil.assertTrue(levelFile != null);
return new DhServerLevel(saveStructure, w);
});
}
@Override
public DhServerLevel getLevel(ILevelWrapper wrapper) {
if (!(wrapper instanceof IServerLevelWrapper)) return null;
return levels.get(wrapper);
}
@Override
public ILevel[] getAllLoadedLevels()
{
ILevel[] array = new ILevel[this.levels.size()];
int i = 0;
for (ILevel level : this.levels.values())
{
array[i] = level;
i++;
}
return array;
}
@Override
public void unloadLevel(ILevelWrapper wrapper) {
if (!(wrapper instanceof IServerLevelWrapper)) return;
if (levels.containsKey(wrapper)) {
LOGGER.info("Unloading level {} ", levels.get(wrapper));
levels.remove(wrapper).close();
}
}
public void serverTick() {
levels.values().forEach(DhServerLevel::serverTick);
}
public void doWorldGen() {
levels.values().forEach(DhServerLevel::doWorldGen);
}
@Override
public CompletableFuture<Void> saveAndFlush() {
return CompletableFuture.allOf(levels.values().stream().map(DhServerLevel::save).toArray(CompletableFuture[]::new));
}
@Override
public void close() {
for (DhServerLevel level : levels.values()) {
LOGGER.info("Unloading level " + level.level.getDimensionType().getDimensionName());
level.close();
}
levels.clear();
LOGGER.info("Closed DhWorld of type {}", environment);
}
}
@@ -0,0 +1,30 @@
package com.seibel.lod.core.a7.world;
import com.seibel.lod.core.a7.level.ILevel;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
import org.apache.logging.log4j.Logger;
import java.io.Closeable;
import java.util.concurrent.CompletableFuture;
public abstract class DhWorld implements Closeable
{
protected static final Logger LOGGER = DhLoggerBuilder.getLogger();
public final WorldEnvironment environment;
protected DhWorld(WorldEnvironment environment) {
this.environment = environment;
}
public abstract ILevel getOrLoadLevel(ILevelWrapper wrapper);
public abstract ILevel getLevel(ILevelWrapper wrapper);
public abstract ILevel[] getAllLoadedLevels();
public abstract void unloadLevel(ILevelWrapper wrapper);
public abstract CompletableFuture<Void> saveAndFlush();
@Override
public abstract void close();
}
@@ -0,0 +1,5 @@
package com.seibel.lod.core.a7.world;
public interface IClientWorld {
void clientTick();
}
@@ -0,0 +1,6 @@
package com.seibel.lod.core.a7.world;
public interface IServerWorld {
void serverTick();
void doWorldGen();
}
@@ -0,0 +1,7 @@
package com.seibel.lod.core.a7.world;
public enum WorldEnvironment {
Client_Only,
Client_Server,
Server_Only
}
@@ -0,0 +1,48 @@
package com.seibel.lod.core.api.external;
import com.seibel.lod.core.ModInfo;
import com.seibel.lod.core.a7.datatype.full.FullDataSource;
/**
* This holds API methods related to version numbers and other unchanging endpoints.
* This shouldn't change between API versions.
*
* @author James Seibel
* @version 2022-4-27
*/
public class DhApiMain
{
/** This version should only be updated when breaking changes are introduced to the DH API */
public static int getApiMajorVersion()
{
return ModInfo.API_MAJOR_VERSION;
}
/** This version should be updated whenever new methods are added to the DH API */
public static int getApiMinorVersion()
{
return ModInfo.API_MINOR_VERSION;
}
/** Returns the mod's version number in the format: Major.Minor.Patch */
public static String getModVersion()
{
return ModInfo.VERSION;
}
/** Returns true if the mod is a development version, false if it is a release version. */
public static boolean getIsDevVersion()
{
return ModInfo.IS_DEV_BUILD;
}
/** Returns the network protocol version. */
public static int getNetworkProtocolVersion()
{
return ModInfo.PROTOCOL_VERSION;
}
/** Returns the LOD file version. */
public static int getLodFileFormatVersion()
{
return FullDataSource.LATEST_VERSION;
}
}
@@ -0,0 +1 @@
The external api package holds any code that interfaces between Distant Horizons and other mods or projects.
@@ -0,0 +1,42 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.api.external.items.enums;
import com.seibel.lod.core.api.external.items.enums.override.DhApiOverrideEnumAssembly;
import com.seibel.lod.core.api.external.items.enums.config.DhApiConfigEnumAssembly;
import com.seibel.lod.core.api.external.items.enums.worldGeneration.DhApiWorldGenerationEnumAssembly;
/**
* Assembly classes are used to reference the package they are in.
*
* @author James Seibel
* @version 2022-7-18
*/
public class DhApiEnumAssembly
{
// These variables are added in order to load each package into the JVM's class loader.
// This is done so they can be found via reflection.
private static final DhApiWorldGenerationEnumAssembly worldGenerationAssembly = new DhApiWorldGenerationEnumAssembly();
private static final DhApiConfigEnumAssembly configAssembly = new DhApiConfigEnumAssembly();
private static final DhApiOverrideEnumAssembly overrideAssembly = new DhApiOverrideEnumAssembly();
/** All DH API enums should have this prefix */
public static final String API_ENUM_PREFIX = "EDhApi";
}
@@ -0,0 +1,31 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.api.external.items.enums.config;
/**
* Assembly classes are used to reference the package they are in.
*
* @author James Seibel
* @version 2022-7-13
*/
public class DhApiConfigEnumAssembly
{
}
@@ -0,0 +1,53 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.api.external.items.enums.config;
/**
* NONE, <br>
* NON_FULL, <br>
* NO_COLLISION, <br>
* BOTH, <br>
*
* @author Leonardo Amato
* @version 2022-7-1
*/
public enum EDhApiBlocksToAvoid
{
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
NONE(false, false),
NON_FULL(true, false),
NO_COLLISION(false, true),
BOTH(true, true);
public final boolean nonFull;
public final boolean noCollision;
EDhApiBlocksToAvoid(boolean nonFull, boolean noCollision)
{
this.nonFull = nonFull;
this.noCollision = noCollision;
}
}
@@ -0,0 +1,39 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.api.external.items.enums.config;
/**
* CONSTANT <br>
* FREQUENT <br>
* NORMAL <br>
* RARE <br> <br>
*
* Determines how fast the buffers should be regenerated
*
* @author Leonardo Amato
* @version 9-25-2021
*/
public enum EDhApiBufferRebuildTimes
{
CONSTANT,
FREQUENT,
NORMAL,
RARE;
}
@@ -0,0 +1,67 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.api.external.items.enums.config;
/**
* OFF, <br>
* SHOW_WIREFRAME, <br>
* SHOW_DETAIL, <br>
* SHOW_DETAIL_WIREFRAME, <br>
* SHOW_GENMODE, <br>
* SHOW_GENMODE_WIREFRAME, <br>
* SHOW_OVERLAPPING_QUADS, <br>
* SHOW_OVERLAPPING_QUADS_WIREFRAME, <br>
*
* @author Leetom
* @author James Seibel
* @version 2022-7-2
*/
public enum EDhApiDebugMode
{
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** LODs are rendered normally */
OFF,
/** LOD draws in wireframe. */
SHOW_WIREFRAME,
/** LOD colors are based on their detail */
SHOW_DETAIL,
/** LOD colors are based on their detail, and draws in wireframe. */
SHOW_DETAIL_WIREFRAME,
/** LOD colors are based on their gen mode. */
SHOW_GENMODE,
/** LOD colors are based on their gen mode, and draws in wireframe. */
SHOW_GENMODE_WIREFRAME,
/** Only draw overlapping LOD quads. */
SHOW_OVERLAPPING_QUADS,
/** Only draw overlapping LOD quads, and draws in wireframe. */
SHOW_OVERLAPPING_QUADS_WIREFRAME;
}
@@ -0,0 +1,88 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.api.external.items.enums.config;
/**
* NONE <br>
* BIOME_ONLY <br>
* BIOME_ONLY_SIMULATE_HEIGHT <br>
* SURFACE <br>
* FEATURES <br>
* FULL <br><br>
*
* In order of fastest to slowest.
*
* @author James Seibel
* @author Leonardo Amato
* @version 2022-7-1
*/
public enum EDhApiDistanceGenerationMode
{
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** Don't generate anything except already existing chunks */
NONE,
/**
* Only generate the biomes and use biome
* grass/foliage color, water color, or ice color
* to generate the color. <br>
* Doesn't generate height, everything is shown at sea level. <br>
* Multithreaded - Fastest (2-5 ms)
*/
BIOME_ONLY,
/**
* Same as BIOME_ONLY, except instead
* of always using sea level as the LOD height
* different biome types (mountain, ocean, forest, etc.)
* use predetermined heights to simulate having height data.
*/
BIOME_ONLY_SIMULATE_HEIGHT,
/**
* Generate the world surface,
* this does NOT include caves, trees,
* or structures. <br>
* Multithreaded - Faster (10-20 ms)
*/
SURFACE,
/**
* Generate including structures.
* NOTE: This may cause world generation bugs or instability,
* since some features can cause concurrentModification exceptions. <br>
* Multithreaded - Fast (15-20 ms)
*/
FEATURES,
/**
* Ask the server to generate/load each chunk.
* This is the most compatible, but causes server/simulation lag.
* This will also show player made structures if you
* are adding the mod on a pre-existing world. <br>
* Single-threaded - Slow (15-50 ms, with spikes up to 200 ms)
*/
FULL;
}
@@ -0,0 +1,54 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2022 Tom Lee (TomTheFurry)
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.api.external.items.enums.config;
/**
* AUTO <br>
* SMOOTH_DROPOFF <br>
* PERFORMANCE_FOCUSED <br> <br>
*
* Determines how lod level drop off should be done
*
* @author Tom Lee
* @version 7-1-2022
*/
public enum EDhApiDropoffQuality
{
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** SMOOTH_DROPOFF when <128 lod view distance, or PERFORMANCE_FOCUSED otherwise */
AUTO(-1),
SMOOTH_DROPOFF(10),
PERFORMANCE_FOCUSED(0);
public final int fastModeSwitch;
EDhApiDropoffQuality(int fastModeSwitch) {
this.fastModeSwitch = fastModeSwitch;
}
}
@@ -0,0 +1,46 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.api.external.items.enums.config;
/**
* USE_DEFAULT_FOG_COLOR, <br>
* USE_SKY_COLOR, <br>
*
* @author James Seibel
* @version 2022-6-9
*/
public enum EDhApiFogColorMode
{
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** Fog uses Minecraft's fog color. */
USE_WORLD_FOG_COLOR,
/**
* Replicates the effect of the clear sky mod.
* Making the fog blend in with the sky better
* For it to look good you need one of the following mods:
* https://www.curseforge.com/minecraft/mc-mods/clear-skies
* https://www.curseforge.com/minecraft/mc-mods/clear-skies-forge-port
*/
USE_SKY_COLOR,
}
@@ -0,0 +1,39 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.api.external.items.enums.config;
/**
* NEAR, <br>
* FAR, <br>
* NEAR_AND_FAR <br>
*
* @author James Seibel
* @version 2022-6-2
*/
public enum EDhApiFogDistance
{
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
NEAR,
FAR,
NEAR_AND_FAR
}
@@ -0,0 +1,46 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.api.external.items.enums.config;
/**
* USE_OPTIFINE_FOG_SETTING, <br>
* FOG_ENABLED, <br>
* FOG_DISABLED <br>
*
* @author James Seibel
* @version 2022-6-2
*/
public enum EDhApiFogDrawMode
{
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/**
* Use whatever Fog setting optifine is using.
* If optifine isn't installed this defaults to FOG_ENABLED.
*/
USE_OPTIFINE_SETTING,
FOG_ENABLED,
FOG_DISABLED;
}
@@ -0,0 +1,21 @@
package com.seibel.lod.core.api.external.items.enums.config;
/**
* LINEAR, <br>
* EXPONENTIAL, <br>
* EXPONENTIAL_SQUARED <br>
*
* @author Leetom
* @version 2022-6-30
*/
public enum EDhApiFogFalloff
{
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
LINEAR,
EXPONENTIAL,
EXPONENTIAL_SQUARED,
}
@@ -0,0 +1,47 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.api.external.items.enums.config;
/**
* AUTO <br>
* Near_First <br>
* Far_First <br> <br>
*
* Determines which LODs should have priority when generating
* outside the normal view distance.
*
* @author Leonardo Amato
* @version 12-1-2021
*/
public enum EDhApiGenerationPriority
{
// Reminder:
// when adding items up the API minor version
// when removing items up the API major version
/** NEAR_FIRST when connected to servers and BALANCED when on single player */
AUTO,
NEAR_FIRST,
BALANCED,
FAR_FIRST
}
@@ -0,0 +1,64 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.api.external.items.enums.config;
/**
* AUTO, <br>
* BUFFER_STORAGE, <br>
* SUB_DATA, <br>
* BUFFER_MAPPING, <br>
* DATA <br>
*
* @author Leetom
* @author James Seibel
* @version 2022-7-2
*/
public enum EDhApiGpuUploadMethod
{
/** Picks the best option based on the GPU the user has. */
AUTO,
/**
* Default for NVIDIA if OpenGL 4.5 is supported. <br>
* Fast rendering, no stuttering.
*/
BUFFER_STORAGE,
/**
* Backup option for NVIDIA. <br>
* Fast rendering but may stutter when uploading.
*/
SUB_DATA,
/**
* Default option for AMD/Intel. <br>
* May end up storing buffers in System memory. <br>
* Fast rending if in GPU memory, slow if in system memory, <br>
* but won't stutter when uploading.
*/
BUFFER_MAPPING,
/**
* Backup option for AMD/Intel. <br>
* Fast rendering but may stutter when uploading.
*/
DATA;
}
@@ -0,0 +1,49 @@
/*
* This file is part of the Distant Horizons mod (formerly the LOD Mod),
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020-2022 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.lod.core.api.external.items.enums.config;
/**
* BASIC <br>
* IGNORE_HEIGHT <br>
* ADDITION <br>
* MAX <br>
* MULTIPLY <br>
* INVERSE_MULTIPLY <br>
* LIMITED_ADDITION <br>
* MULTIPLY_ADDITION <br>
* INVERSE_MULTIPLY_ADDITION <br>
* AVERAGE <br>
*
* @author Leetom
* @version 2022-4-14
*/
public enum EDhApiHeightFogMixMode
{
BASIC,
IGNORE_HEIGHT,
ADDITION,
MAX,
MULTIPLY,
INVERSE_MULTIPLY,
LIMITED_ADDITION,
MULTIPLY_ADDITION,
INVERSE_MULTIPLY_ADDITION,
AVERAGE,
}

Some files were not shown because too many files have changed in this diff Show More