437 lines
22 KiB
Java
437 lines
22 KiB
Java
/*
|
|
* Decompiled with CFR 0.152.
|
|
*
|
|
* Could not load the following classes:
|
|
* com.google.common.collect.Lists
|
|
* com.google.common.collect.Queues
|
|
* com.mojang.logging.LogUtils
|
|
* it.unimi.dsi.fastutil.longs.Long2ObjectMap
|
|
* it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
|
|
* it.unimi.dsi.fastutil.longs.LongIterator
|
|
* it.unimi.dsi.fastutil.longs.LongOpenHashSet
|
|
* it.unimi.dsi.fastutil.longs.LongSet
|
|
* org.joml.Vector3d
|
|
* org.joml.Vector3dc
|
|
* org.jspecify.annotations.Nullable
|
|
* org.slf4j.Logger
|
|
*/
|
|
package net.minecraft.client.renderer;
|
|
|
|
import com.google.common.collect.Lists;
|
|
import com.google.common.collect.Queues;
|
|
import com.mojang.logging.LogUtils;
|
|
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
|
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
|
import it.unimi.dsi.fastutil.longs.LongIterator;
|
|
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
|
import it.unimi.dsi.fastutil.longs.LongSet;
|
|
import java.util.ArrayDeque;
|
|
import java.util.ArrayList;
|
|
import java.util.Comparator;
|
|
import java.util.List;
|
|
import java.util.Queue;
|
|
import java.util.concurrent.BlockingQueue;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.Future;
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
import java.util.function.Consumer;
|
|
import net.minecraft.client.Camera;
|
|
import net.minecraft.client.renderer.LevelRenderer;
|
|
import net.minecraft.client.renderer.Octree;
|
|
import net.minecraft.client.renderer.ViewArea;
|
|
import net.minecraft.client.renderer.chunk.CompiledSectionMesh;
|
|
import net.minecraft.client.renderer.chunk.SectionMesh;
|
|
import net.minecraft.client.renderer.chunk.SectionRenderDispatcher;
|
|
import net.minecraft.client.renderer.culling.Frustum;
|
|
import net.minecraft.core.BlockPos;
|
|
import net.minecraft.core.Direction;
|
|
import net.minecraft.core.SectionPos;
|
|
import net.minecraft.server.level.ChunkTrackingView;
|
|
import net.minecraft.util.Mth;
|
|
import net.minecraft.util.Util;
|
|
import net.minecraft.util.VisibleForDebug;
|
|
import net.minecraft.world.level.ChunkPos;
|
|
import net.minecraft.world.level.LevelHeightAccessor;
|
|
import net.minecraft.world.phys.Vec3;
|
|
import org.joml.Vector3d;
|
|
import org.joml.Vector3dc;
|
|
import org.jspecify.annotations.Nullable;
|
|
import org.slf4j.Logger;
|
|
|
|
public class SectionOcclusionGraph {
|
|
private static final Logger LOGGER = LogUtils.getLogger();
|
|
private static final Direction[] DIRECTIONS = Direction.values();
|
|
private static final int MINIMUM_ADVANCED_CULLING_DISTANCE = 60;
|
|
private static final int MINIMUM_ADVANCED_CULLING_SECTION_DISTANCE = SectionPos.blockToSectionCoord(60);
|
|
private static final double CEILED_SECTION_DIAGONAL = Math.ceil(Math.sqrt(3.0) * 16.0);
|
|
private boolean needsFullUpdate = true;
|
|
private @Nullable Future<?> fullUpdateTask;
|
|
private @Nullable ViewArea viewArea;
|
|
private final AtomicReference<@Nullable GraphState> currentGraph = new AtomicReference();
|
|
private final AtomicReference<@Nullable GraphEvents> nextGraphEvents = new AtomicReference();
|
|
private final AtomicBoolean needsFrustumUpdate = new AtomicBoolean(false);
|
|
|
|
public void waitAndReset(@Nullable ViewArea viewArea) {
|
|
if (this.fullUpdateTask != null) {
|
|
try {
|
|
this.fullUpdateTask.get();
|
|
this.fullUpdateTask = null;
|
|
}
|
|
catch (Exception e) {
|
|
LOGGER.warn("Full update failed", (Throwable)e);
|
|
}
|
|
}
|
|
this.viewArea = viewArea;
|
|
if (viewArea != null) {
|
|
this.currentGraph.set(new GraphState(viewArea));
|
|
this.invalidate();
|
|
} else {
|
|
this.currentGraph.set(null);
|
|
}
|
|
}
|
|
|
|
public void invalidate() {
|
|
this.needsFullUpdate = true;
|
|
}
|
|
|
|
public void addSectionsInFrustum(Frustum frustum, List<SectionRenderDispatcher.RenderSection> visibleSections, List<SectionRenderDispatcher.RenderSection> nearbyVisibleSection) {
|
|
this.currentGraph.get().storage().sectionTree.visitNodes((node, fullyVisible, depth, isClose) -> {
|
|
SectionRenderDispatcher.RenderSection renderSection = node.getSection();
|
|
if (renderSection != null) {
|
|
visibleSections.add(renderSection);
|
|
if (isClose) {
|
|
nearbyVisibleSection.add(renderSection);
|
|
}
|
|
}
|
|
}, frustum, 32);
|
|
}
|
|
|
|
public boolean consumeFrustumUpdate() {
|
|
return this.needsFrustumUpdate.compareAndSet(true, false);
|
|
}
|
|
|
|
public void onChunkReadyToRender(ChunkPos pos) {
|
|
GraphEvents events;
|
|
GraphEvents nextEvents = this.nextGraphEvents.get();
|
|
if (nextEvents != null) {
|
|
this.addNeighbors(nextEvents, pos);
|
|
}
|
|
if ((events = this.currentGraph.get().events) != nextEvents) {
|
|
this.addNeighbors(events, pos);
|
|
}
|
|
}
|
|
|
|
public void schedulePropagationFrom(SectionRenderDispatcher.RenderSection section) {
|
|
GraphEvents events;
|
|
GraphEvents nextEvents = this.nextGraphEvents.get();
|
|
if (nextEvents != null) {
|
|
nextEvents.sectionsToPropagateFrom.add(section);
|
|
}
|
|
if ((events = this.currentGraph.get().events) != nextEvents) {
|
|
events.sectionsToPropagateFrom.add(section);
|
|
}
|
|
}
|
|
|
|
public void update(boolean smartCull, Camera camera, Frustum frustum, List<SectionRenderDispatcher.RenderSection> visibleSections, LongOpenHashSet loadedEmptySections) {
|
|
Vec3 cameraPos = camera.position();
|
|
if (this.needsFullUpdate && (this.fullUpdateTask == null || this.fullUpdateTask.isDone())) {
|
|
this.scheduleFullUpdate(smartCull, camera, cameraPos, loadedEmptySections);
|
|
}
|
|
this.runPartialUpdate(smartCull, frustum, visibleSections, cameraPos, loadedEmptySections);
|
|
}
|
|
|
|
private void scheduleFullUpdate(boolean smartCull, Camera camera, Vec3 cameraPos, LongOpenHashSet loadedEmptySections) {
|
|
this.needsFullUpdate = false;
|
|
LongOpenHashSet emptySections = loadedEmptySections.clone();
|
|
this.fullUpdateTask = CompletableFuture.runAsync(() -> {
|
|
GraphState newState = new GraphState(this.viewArea);
|
|
this.nextGraphEvents.set(newState.events);
|
|
ArrayDeque queue = Queues.newArrayDeque();
|
|
this.initializeQueueForFullUpdate(camera, queue);
|
|
queue.forEach(node -> newState.storage.sectionToNodeMap.put(node.section, (Node)node));
|
|
this.runUpdates(newState.storage, cameraPos, queue, smartCull, node -> {}, emptySections);
|
|
this.currentGraph.set(newState);
|
|
this.nextGraphEvents.set(null);
|
|
this.needsFrustumUpdate.set(true);
|
|
}, Util.backgroundExecutor());
|
|
}
|
|
|
|
private void runPartialUpdate(boolean smartCull, Frustum frustum, List<SectionRenderDispatcher.RenderSection> visibleSections, Vec3 cameraPos, LongOpenHashSet loadedEmptySections) {
|
|
GraphState state = this.currentGraph.get();
|
|
this.queueSectionsWithNewNeighbors(state);
|
|
if (!state.events.sectionsToPropagateFrom.isEmpty()) {
|
|
ArrayDeque queue = Queues.newArrayDeque();
|
|
while (!state.events.sectionsToPropagateFrom.isEmpty()) {
|
|
SectionRenderDispatcher.RenderSection renderSection = (SectionRenderDispatcher.RenderSection)state.events.sectionsToPropagateFrom.poll();
|
|
Node node = state.storage.sectionToNodeMap.get(renderSection);
|
|
if (node == null || node.section != renderSection) continue;
|
|
queue.add(node);
|
|
}
|
|
Frustum offsetFrustum = LevelRenderer.offsetFrustum(frustum);
|
|
Consumer<SectionRenderDispatcher.RenderSection> onSectionAdded = section -> {
|
|
if (offsetFrustum.isVisible(section.getBoundingBox())) {
|
|
this.needsFrustumUpdate.set(true);
|
|
}
|
|
};
|
|
this.runUpdates(state.storage, cameraPos, queue, smartCull, onSectionAdded, loadedEmptySections);
|
|
}
|
|
}
|
|
|
|
private void queueSectionsWithNewNeighbors(GraphState state) {
|
|
LongIterator iterator = state.events.chunksWhichReceivedNeighbors.iterator();
|
|
while (iterator.hasNext()) {
|
|
long chunkWithNewNeighbor = iterator.nextLong();
|
|
List renderSections = (List)state.storage.chunksWaitingForNeighbors.get(chunkWithNewNeighbor);
|
|
if (renderSections == null || !((SectionRenderDispatcher.RenderSection)renderSections.get(0)).hasAllNeighbors()) continue;
|
|
state.events.sectionsToPropagateFrom.addAll(renderSections);
|
|
state.storage.chunksWaitingForNeighbors.remove(chunkWithNewNeighbor);
|
|
}
|
|
state.events.chunksWhichReceivedNeighbors.clear();
|
|
}
|
|
|
|
private void addNeighbors(GraphEvents events, ChunkPos pos) {
|
|
events.chunksWhichReceivedNeighbors.add(ChunkPos.asLong(pos.x - 1, pos.z));
|
|
events.chunksWhichReceivedNeighbors.add(ChunkPos.asLong(pos.x, pos.z - 1));
|
|
events.chunksWhichReceivedNeighbors.add(ChunkPos.asLong(pos.x + 1, pos.z));
|
|
events.chunksWhichReceivedNeighbors.add(ChunkPos.asLong(pos.x, pos.z + 1));
|
|
events.chunksWhichReceivedNeighbors.add(ChunkPos.asLong(pos.x - 1, pos.z - 1));
|
|
events.chunksWhichReceivedNeighbors.add(ChunkPos.asLong(pos.x - 1, pos.z + 1));
|
|
events.chunksWhichReceivedNeighbors.add(ChunkPos.asLong(pos.x + 1, pos.z - 1));
|
|
events.chunksWhichReceivedNeighbors.add(ChunkPos.asLong(pos.x + 1, pos.z + 1));
|
|
}
|
|
|
|
private void initializeQueueForFullUpdate(Camera camera, Queue<Node> queue) {
|
|
BlockPos cameraPosition = camera.blockPosition();
|
|
long cameraSectionNode = SectionPos.asLong(cameraPosition);
|
|
int cameraSectionY = SectionPos.y(cameraSectionNode);
|
|
SectionRenderDispatcher.RenderSection cameraSection = this.viewArea.getRenderSection(cameraSectionNode);
|
|
if (cameraSection == null) {
|
|
LevelHeightAccessor heightAccessor = this.viewArea.getLevelHeightAccessor();
|
|
boolean isBelowTheWorld = cameraSectionY < heightAccessor.getMinSectionY();
|
|
int sectionY = isBelowTheWorld ? heightAccessor.getMinSectionY() : heightAccessor.getMaxSectionY();
|
|
int viewDistance = this.viewArea.getViewDistance();
|
|
ArrayList toAdd = Lists.newArrayList();
|
|
int cameraSectionX = SectionPos.x(cameraSectionNode);
|
|
int cameraSectionZ = SectionPos.z(cameraSectionNode);
|
|
for (int sectionX = -viewDistance; sectionX <= viewDistance; ++sectionX) {
|
|
for (int sectionZ = -viewDistance; sectionZ <= viewDistance; ++sectionZ) {
|
|
SectionRenderDispatcher.RenderSection renderSectionAt = this.viewArea.getRenderSection(SectionPos.asLong(sectionX + cameraSectionX, sectionY, sectionZ + cameraSectionZ));
|
|
if (renderSectionAt == null || !this.isInViewDistance(cameraSectionNode, renderSectionAt.getSectionNode())) continue;
|
|
Direction sourceDirection = isBelowTheWorld ? Direction.UP : Direction.DOWN;
|
|
Node node = new Node(renderSectionAt, sourceDirection, 0);
|
|
node.setDirections(node.directions, sourceDirection);
|
|
if (sectionX > 0) {
|
|
node.setDirections(node.directions, Direction.EAST);
|
|
} else if (sectionX < 0) {
|
|
node.setDirections(node.directions, Direction.WEST);
|
|
}
|
|
if (sectionZ > 0) {
|
|
node.setDirections(node.directions, Direction.SOUTH);
|
|
} else if (sectionZ < 0) {
|
|
node.setDirections(node.directions, Direction.NORTH);
|
|
}
|
|
toAdd.add(node);
|
|
}
|
|
}
|
|
toAdd.sort(Comparator.comparingDouble(c -> cameraPosition.distSqr(SectionPos.of(c.section.getSectionNode()).center())));
|
|
queue.addAll(toAdd);
|
|
} else {
|
|
queue.add(new Node(cameraSection, null, 0));
|
|
}
|
|
}
|
|
|
|
private void runUpdates(GraphStorage storage, Vec3 cameraPos, Queue<Node> queue, boolean smartCull, Consumer<SectionRenderDispatcher.RenderSection> onSectionAdded, LongOpenHashSet emptySections) {
|
|
SectionPos cameraSectionPos = SectionPos.of(cameraPos);
|
|
long cameraSectionNode = cameraSectionPos.asLong();
|
|
BlockPos cameraSectionCenter = cameraSectionPos.center();
|
|
while (!queue.isEmpty()) {
|
|
long sectionNode;
|
|
Node node = queue.poll();
|
|
SectionRenderDispatcher.RenderSection currentSection = node.section;
|
|
if (!emptySections.contains(node.section.getSectionNode())) {
|
|
if (storage.sectionTree.add(node.section)) {
|
|
onSectionAdded.accept(node.section);
|
|
}
|
|
} else {
|
|
node.section.sectionMesh.compareAndSet(CompiledSectionMesh.UNCOMPILED, CompiledSectionMesh.EMPTY);
|
|
}
|
|
boolean distantFromCamera = Math.abs(SectionPos.x(sectionNode = currentSection.getSectionNode()) - cameraSectionPos.x()) > MINIMUM_ADVANCED_CULLING_SECTION_DISTANCE || Math.abs(SectionPos.y(sectionNode) - cameraSectionPos.y()) > MINIMUM_ADVANCED_CULLING_SECTION_DISTANCE || Math.abs(SectionPos.z(sectionNode) - cameraSectionPos.z()) > MINIMUM_ADVANCED_CULLING_SECTION_DISTANCE;
|
|
for (Direction direction : DIRECTIONS) {
|
|
Node existingNode;
|
|
SectionRenderDispatcher.RenderSection renderSectionAt = this.getRelativeFrom(cameraSectionNode, currentSection, direction);
|
|
if (renderSectionAt == null || smartCull && node.hasDirection(direction.getOpposite())) continue;
|
|
if (smartCull && node.hasSourceDirections()) {
|
|
SectionMesh sectionMesh = currentSection.getSectionMesh();
|
|
boolean visible = false;
|
|
for (int i = 0; i < DIRECTIONS.length; ++i) {
|
|
if (!node.hasSourceDirection(i) || !sectionMesh.facesCanSeeEachother(DIRECTIONS[i].getOpposite(), direction)) continue;
|
|
visible = true;
|
|
break;
|
|
}
|
|
if (!visible) continue;
|
|
}
|
|
if (smartCull && distantFromCamera) {
|
|
boolean maxY;
|
|
boolean maxX;
|
|
int renderSectionOriginX = SectionPos.sectionToBlockCoord(SectionPos.x(sectionNode));
|
|
int renderSectionOriginY = SectionPos.sectionToBlockCoord(SectionPos.y(sectionNode));
|
|
int renderSectionOriginZ = SectionPos.sectionToBlockCoord(SectionPos.z(sectionNode));
|
|
boolean bl = direction.getAxis() == Direction.Axis.X ? cameraSectionCenter.getX() > renderSectionOriginX : (maxX = cameraSectionCenter.getX() < renderSectionOriginX);
|
|
boolean bl2 = direction.getAxis() == Direction.Axis.Y ? cameraSectionCenter.getY() > renderSectionOriginY : (maxY = cameraSectionCenter.getY() < renderSectionOriginY);
|
|
boolean maxZ = direction.getAxis() == Direction.Axis.Z ? cameraSectionCenter.getZ() > renderSectionOriginZ : cameraSectionCenter.getZ() < renderSectionOriginZ;
|
|
Vector3d checkPos = new Vector3d((double)(renderSectionOriginX + (maxX ? 16 : 0)), (double)(renderSectionOriginY + (maxY ? 16 : 0)), (double)(renderSectionOriginZ + (maxZ ? 16 : 0)));
|
|
Vector3d step = new Vector3d(cameraPos.x, cameraPos.y, cameraPos.z).sub((Vector3dc)checkPos).normalize().mul(CEILED_SECTION_DIAGONAL);
|
|
boolean visible = true;
|
|
while (checkPos.distanceSquared(cameraPos.x, cameraPos.y, cameraPos.z) > 3600.0) {
|
|
checkPos.add((Vector3dc)step);
|
|
LevelHeightAccessor heightAccessor = this.viewArea.getLevelHeightAccessor();
|
|
if (checkPos.y > (double)heightAccessor.getMaxY() || checkPos.y < (double)heightAccessor.getMinY()) break;
|
|
SectionRenderDispatcher.RenderSection checkSection = this.viewArea.getRenderSectionAt(BlockPos.containing(checkPos.x, checkPos.y, checkPos.z));
|
|
if (checkSection != null && storage.sectionToNodeMap.get(checkSection) != null) continue;
|
|
visible = false;
|
|
break;
|
|
}
|
|
if (!visible) continue;
|
|
}
|
|
if ((existingNode = storage.sectionToNodeMap.get(renderSectionAt)) != null) {
|
|
existingNode.addSourceDirection(direction);
|
|
continue;
|
|
}
|
|
Node newNode = new Node(renderSectionAt, direction, node.step + 1);
|
|
newNode.setDirections(node.directions, direction);
|
|
if (renderSectionAt.hasAllNeighbors()) {
|
|
queue.add(newNode);
|
|
storage.sectionToNodeMap.put(renderSectionAt, newNode);
|
|
continue;
|
|
}
|
|
if (!this.isInViewDistance(cameraSectionNode, renderSectionAt.getSectionNode())) continue;
|
|
storage.sectionToNodeMap.put(renderSectionAt, newNode);
|
|
long chunkNode = SectionPos.sectionToChunk(renderSectionAt.getSectionNode());
|
|
((List)storage.chunksWaitingForNeighbors.computeIfAbsent(chunkNode, l -> new ArrayList())).add(renderSectionAt);
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean isInViewDistance(long cameraSectionNode, long sectionNode) {
|
|
return ChunkTrackingView.isInViewDistance(SectionPos.x(cameraSectionNode), SectionPos.z(cameraSectionNode), this.viewArea.getViewDistance(), SectionPos.x(sectionNode), SectionPos.z(sectionNode));
|
|
}
|
|
|
|
private @Nullable SectionRenderDispatcher.RenderSection getRelativeFrom(long cameraSectionNode, SectionRenderDispatcher.RenderSection renderSection, Direction direction) {
|
|
long relative = renderSection.getNeighborSectionNode(direction);
|
|
if (!this.isInViewDistance(cameraSectionNode, relative)) {
|
|
return null;
|
|
}
|
|
if (Mth.abs(SectionPos.y(cameraSectionNode) - SectionPos.y(relative)) > this.viewArea.getViewDistance()) {
|
|
return null;
|
|
}
|
|
return this.viewArea.getRenderSection(relative);
|
|
}
|
|
|
|
@VisibleForDebug
|
|
public @Nullable Node getNode(SectionRenderDispatcher.RenderSection section) {
|
|
return this.currentGraph.get().storage.sectionToNodeMap.get(section);
|
|
}
|
|
|
|
public Octree getOctree() {
|
|
return this.currentGraph.get().storage.sectionTree;
|
|
}
|
|
|
|
private record GraphState(GraphStorage storage, GraphEvents events) {
|
|
private GraphState(ViewArea viewArea) {
|
|
this(new GraphStorage(viewArea), new GraphEvents());
|
|
}
|
|
}
|
|
|
|
private static class GraphStorage {
|
|
public final SectionToNodeMap sectionToNodeMap;
|
|
public final Octree sectionTree;
|
|
public final Long2ObjectMap<List<SectionRenderDispatcher.RenderSection>> chunksWaitingForNeighbors;
|
|
|
|
public GraphStorage(ViewArea viewArea) {
|
|
this.sectionToNodeMap = new SectionToNodeMap(viewArea.sections.length);
|
|
this.sectionTree = new Octree(viewArea.getCameraSectionPos(), viewArea.getViewDistance(), viewArea.sectionGridSizeY, viewArea.level.getMinY());
|
|
this.chunksWaitingForNeighbors = new Long2ObjectOpenHashMap();
|
|
}
|
|
}
|
|
|
|
private record GraphEvents(LongSet chunksWhichReceivedNeighbors, BlockingQueue<SectionRenderDispatcher.RenderSection> sectionsToPropagateFrom) {
|
|
private GraphEvents() {
|
|
this((LongSet)new LongOpenHashSet(), new LinkedBlockingQueue<SectionRenderDispatcher.RenderSection>());
|
|
}
|
|
}
|
|
|
|
private static class SectionToNodeMap {
|
|
private final Node[] nodes;
|
|
|
|
private SectionToNodeMap(int sectionCount) {
|
|
this.nodes = new Node[sectionCount];
|
|
}
|
|
|
|
public void put(SectionRenderDispatcher.RenderSection renderSection, Node node) {
|
|
this.nodes[renderSection.index] = node;
|
|
}
|
|
|
|
public @Nullable Node get(SectionRenderDispatcher.RenderSection renderSection) {
|
|
int index = renderSection.index;
|
|
if (index < 0 || index >= this.nodes.length) {
|
|
return null;
|
|
}
|
|
return this.nodes[index];
|
|
}
|
|
}
|
|
|
|
@VisibleForDebug
|
|
public static class Node {
|
|
@VisibleForDebug
|
|
protected final SectionRenderDispatcher.RenderSection section;
|
|
private byte sourceDirections;
|
|
private byte directions;
|
|
@VisibleForDebug
|
|
public final int step;
|
|
|
|
private Node(SectionRenderDispatcher.RenderSection section, @Nullable Direction sourceDirection, int step) {
|
|
this.section = section;
|
|
if (sourceDirection != null) {
|
|
this.addSourceDirection(sourceDirection);
|
|
}
|
|
this.step = step;
|
|
}
|
|
|
|
private void setDirections(byte oldDirections, Direction direction) {
|
|
this.directions = (byte)(this.directions | (oldDirections | 1 << direction.ordinal()));
|
|
}
|
|
|
|
private boolean hasDirection(Direction direction) {
|
|
return (this.directions & 1 << direction.ordinal()) > 0;
|
|
}
|
|
|
|
private void addSourceDirection(Direction direction) {
|
|
this.sourceDirections = (byte)(this.sourceDirections | (this.sourceDirections | 1 << direction.ordinal()));
|
|
}
|
|
|
|
@VisibleForDebug
|
|
public boolean hasSourceDirection(int directionOrdinal) {
|
|
return (this.sourceDirections & 1 << directionOrdinal) > 0;
|
|
}
|
|
|
|
private boolean hasSourceDirections() {
|
|
return this.sourceDirections != 0;
|
|
}
|
|
|
|
public int hashCode() {
|
|
return Long.hashCode(this.section.getSectionNode());
|
|
}
|
|
|
|
public boolean equals(Object obj) {
|
|
if (!(obj instanceof Node)) {
|
|
return false;
|
|
}
|
|
Node other = (Node)obj;
|
|
return this.section.getSectionNode() == other.section.getSectionNode();
|
|
}
|
|
}
|
|
}
|
|
|