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:
@@ -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;
|
||||
}
|
||||
}
|
||||
+132
@@ -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));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+130
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -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);
|
||||
}
|
||||
+36
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+392
@@ -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);
|
||||
}
|
||||
+99
@@ -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);
|
||||
}
|
||||
}
|
||||
+31
@@ -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);
|
||||
}
|
||||
}
|
||||
+412
@@ -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;
|
||||
}
|
||||
}
|
||||
+167
@@ -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);
|
||||
}
|
||||
+347
@@ -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.
|
||||
+42
@@ -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";
|
||||
}
|
||||
Vendored
+31
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
Vendored
+53
@@ -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;
|
||||
}
|
||||
}
|
||||
core/src/main/java/com/seibel/lod/core/api/external/items/enums/config/EDhApiBufferRebuildTimes.java
Vendored
+39
@@ -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;
|
||||
}
|
||||
Vendored
+67
@@ -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;
|
||||
|
||||
}
|
||||
+88
@@ -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;
|
||||
|
||||
}
|
||||
Vendored
+54
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Vendored
+46
@@ -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,
|
||||
}
|
||||
Vendored
+39
@@ -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
|
||||
}
|
||||
Vendored
+46
@@ -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;
|
||||
|
||||
}
|
||||
Vendored
+21
@@ -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,
|
||||
}
|
||||
core/src/main/java/com/seibel/lod/core/api/external/items/enums/config/EDhApiGenerationPriority.java
Vendored
+47
@@ -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
|
||||
}
|
||||
Vendored
+64
@@ -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;
|
||||
|
||||
}
|
||||
Vendored
+49
@@ -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
Reference in New Issue
Block a user