Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 215 additions & 31 deletions src/main/java/net/tfminecraft/simplefactions/guild/hub/Highway.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ public double boostedProductionFactor(double bonus) {
private static volatile Map<Mode, Rates> rates;

static {
DEFAULTS.put(Mode.RAIL, new Rates(0.40, 0.80, 0.90));
DEFAULTS.put(Mode.SEA, new Rates(0.30, 0.70, 0.85));
DEFAULTS.put(Mode.AIR, new Rates(0.20, 0.50, 0.80));
DEFAULTS.put(Mode.RAIL, new Rates(0.40, 0.40, 0.90));
DEFAULTS.put(Mode.SEA, new Rates(0.30, 0.20, 0.85));
DEFAULTS.put(Mode.AIR, new Rates(0.20, 0.0, 0.80));
rates = new EnumMap<>(DEFAULTS);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package net.tfminecraft.simplefactions.guild.hub;

import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;

/** How far a lone train station pushes trade along track that has no second station. */
public final class OpenTrackSettings {
public static final double DEFAULT_SHARE = 0.75;
public static final double DEFAULT_KEPT = 0.85;
public static final double DEFAULT_RANGE = 2500;

private static volatile boolean enabled = true;
private static volatile double share = DEFAULT_SHARE;
private static volatile double keptPer1000 = DEFAULT_KEPT;
private static volatile double rangeBlocks = DEFAULT_RANGE;

private OpenTrackSettings() {
}

public static void load(FileConfiguration config) {
ConfigurationSection open = config == null
? null : config.getConfigurationSection("installation-trade.open-track");
enabled = open == null || open.getBoolean("enabled", true);
share = nonNegative(open == null ? DEFAULT_SHARE : open.getDouble("share", DEFAULT_SHARE), DEFAULT_SHARE);
keptPer1000 = kept(open == null
? DEFAULT_KEPT : open.getDouble("kept-per-1000-blocks", DEFAULT_KEPT));
double range = open == null ? DEFAULT_RANGE : open.getDouble("range-blocks", DEFAULT_RANGE);
rangeBlocks = Double.isFinite(range) && range > 0 ? range : 0;
}

public static void reset() {
enabled = true;
share = DEFAULT_SHARE;
keptPer1000 = DEFAULT_KEPT;
rangeBlocks = DEFAULT_RANGE;
}

public static boolean enabled() {
return enabled;
}

/** Multiplied by the rail trade share. The product is capped at {@link HubTransport#MAX_SHARE}. */
public static double share() {
return share;
}

public static double keptPer1000() {
return keptPer1000;
}

public static double rangeBlocks() {
return rangeBlocks;
}

private static double nonNegative(double value, double fallback) {
if (!Double.isFinite(value) || value < 0) {
return fallback;
}
return value;
}

/** Same cap as a line, so distance loss cannot be turned off by config. */
private static double kept(double value) {
if (!Double.isFinite(value) || value < 0) {
return DEFAULT_KEPT;
}
return Math.min(value, HubTransport.MAX_SHARE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package net.tfminecraft.simplefactions.guild.hub;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;

/** Shortest distances along an undirected track graph, limited to a range. */
public final class TrackReach {
public interface At {
int province(double x, double z);
}

record Edge(int to, double weight) {
}

private record Step(int node, double distance) {
}

private TrackReach() {
}

static void link(List<List<Edge>> adjacent, int from, int to, double weight) {
if (from == to || !Double.isFinite(weight) || weight < 0) {
return;
}
adjacent.get(from).add(new Edge(to, weight));
adjacent.get(to).add(new Edge(from, weight));
}

/** Unreached nodes stay at positive infinity. Nodes past {@code range} are not expanded. */
static double[] travel(List<List<Edge>> adjacent, boolean[] source, double range) {
int count = adjacent.size();
double[] distance = new double[count];
Arrays.fill(distance, Double.POSITIVE_INFINITY);
if (!(range >= 0)) {
return distance;
}
PriorityQueue<Step> queue = new PriorityQueue<>(Comparator.comparingDouble(Step::distance));
for (int index = 0; index < count && index < source.length; index++) {
if (!source[index]) {
continue;
}
distance[index] = 0;
queue.add(new Step(index, 0));
}
while (!queue.isEmpty()) {
Step step = queue.poll();
if (step.distance != distance[step.node] || step.distance > range) {
continue;
}
for (Edge edge : adjacent.get(step.node)) {
double next = step.distance + edge.weight;
if (next <= range && next < distance[edge.to]) {
distance[edge.to] = next;
queue.add(new Step(edge.to, next));
}
}
}
return distance;
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
package net.tfminecraft.simplefactions.guild.hub;

import java.util.OptionalDouble;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.Set;
import java.util.UUID;

import org.bukkit.Bukkit;

import net.tfminecraft.simplefactions.guild.network.RailRoutes.Route;
import net.tfminecraft.simplefactions.guild.network.TradeGraph;
import net.tfminecraft.simplefactions.installation.Installation;
import net.tfminecraft.simplefactions.installation.InstallationKind;
import net.tfminecraft.simplefactions.loaders.InstallationConfigLoader;
import net.tfminecraft.simplefactions.map.ProvinceGrid;
import net.tfminecraft.simplefactions.map.infra.TrackProvinceLookup;
import net.tfminecraft.simplefactions.map.infra.TrackProvinceLookup.Point;
import net.tfminecraft.simplefactions.map.provinces.Province;
import net.tfminecraft.vehicleframework.VehicleFramework;
import net.tfminecraft.vehicleframework.tracks.TrackJunction;
import net.tfminecraft.vehicleframework.tracks.TrackPose;
import net.tfminecraft.vehicleframework.tracks.TrackRegistry;
import net.tfminecraft.vehicleframework.tracks.TrackSample;
import net.tfminecraft.vehicleframework.tracks.TrackSpline;

/**
* The only class here that names VehicleFramework types, so it is loaded only once
Expand Down Expand Up @@ -62,4 +73,176 @@ static Set<Integer> sample(String world, ProvinceGrid grid, Map<Integer, Provinc
return TrackProvinceLookup.collect(grid, tracks.sampleTrack(world, 8).stream()
.map(point -> new Point(point.x(), point.y(), point.z())).toList(), provinces);
}

/** Same spacing the station-to-station route uses when it names provinces. */
private static final double SAMPLE_SPACING = 8;
private static final double EPS = 1e-6;

/**
* Along-track distance from each finished train station, measured on this thread
* with the route read. The registry is not a snapshot off the server thread.
*/
public static List<TradeGraph.OpenTrack> openTracks(
String world, Iterable<TradeGraph.Node> stations, double radius, double range, TrackReach.At provinceAt) {
if (world == null || stations == null || provinceAt == null || !(radius >= 0) || !(range > 0)) {
return List.of();
}
TrackRegistry tracks = VehicleFramework.getTrackRegistry();
if (tracks == null) return List.of();
List<TrackSpline> splines = tracks.inWorld(world);
if (splines.isEmpty()) return List.of();
List<TrackJunction> junctions = new ArrayList<>();
for (TrackSpline spline : splines) {
if (spline == null) continue;
List<TrackJunction> onStem = tracks.junctionsOn(spline.getId());
if (onStem != null) junctions.addAll(onStem);
}
Network network = network(splines, junctions);
List<TradeGraph.OpenTrack> found = new ArrayList<>();
for (TradeGraph.Node station : stations) {
if (station == null || station.kind() != InstallationKind.TRAIN_STATION) continue;
found.add(new TradeGraph.OpenTrack(
station.ownerFactionId(),
station.installationId(),
cover(network, station.centerX(), station.centerZ(), radius, range, provinceAt, station.provinceId())));
}
return found;
}

/** Distances for one station. Tests pass splines directly so the registry is not required. */
static Map<Integer, Double> reach(
Collection<TrackSpline> splines, Collection<TrackJunction> junctions,
double x, double z, double radius, double range, TrackReach.At provinceAt, int home) {
if (splines == null || provinceAt == null) return Map.of();
return cover(network(splines, junctions == null ? List.of() : junctions),
x, z, radius, range, provinceAt, home);
}

private record SplineEdge(TrackSpline spline, double startS, double endS, double weight, int from, int to) {
}

private record Network(List<double[]> xy, List<List<TrackReach.Edge>> adjacent, List<SplineEdge> edges) {
}

private static Network network(Collection<TrackSpline> splines, Collection<TrackJunction> junctions) {
List<double[]> xy = new ArrayList<>();
List<List<TrackReach.Edge>> adjacent = new ArrayList<>();
List<SplineEdge> edges = new ArrayList<>();
Map<UUID, Integer> base = new HashMap<>();
Map<UUID, TrackSpline> byId = new HashMap<>();
for (TrackSpline spline : splines) {
if (spline == null || spline.getId() == null || spline.getSamples().size() < 2) continue;
byId.put(spline.getId(), spline);
int origin = xy.size();
base.put(spline.getId(), origin);
for (TrackSample sample : spline.getSamples()) {
xy.add(new double[] {sample.x, sample.z});
adjacent.add(new ArrayList<>());
}
int count = spline.getSamples().size();
for (int edge = 0; edge < spline.edgeCount(); edge++) {
if (spline.segment(edge).broken) continue;
int from = origin + edge;
int to = origin + ((edge + 1) % count);
double start = startS(spline, edge);
double end = endS(spline, edge);
double weight = Math.abs(end - start);
if (weight == 0) {
TrackSample left = spline.getSamples().get(edge % count);
TrackSample right = spline.getSamples().get((edge + 1) % count);
weight = Math.hypot(left.x - right.x, left.z - right.z);
}
TrackReach.link(adjacent, from, to, weight);
edges.add(new SplineEdge(spline, start, end, weight, from, to));
}
}
for (TrackJunction junction : junctions) {
if (junction == null || junction.stemSplineId == null || junction.branchSplineId == null
|| junction.stemSplineId.equals(junction.branchSplineId)) {
continue;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
TrackSpline stem = byId.get(junction.stemSplineId);
TrackSpline branch = byId.get(junction.branchSplineId);
if (stem == null || branch == null) continue;
Integer stemBase = base.get(stem.getId());
Integer branchBase = base.get(branch.getId());
if (stemBase == null || branchBase == null) continue;
double at = TrackJunction.wrapS(junction.s, stem.length(), stem.isLoop());
int count = stem.getSamples().size();
for (int edge = 0; edge < stem.edgeCount(); edge++) {
if (stem.segment(edge).broken) continue;
double start = startS(stem, edge);
double end = endS(stem, edge);
if (at < start - EPS || at > end + EPS) continue;
TrackReach.link(adjacent, stemBase + edge, branchBase, Math.abs(at - start));
TrackReach.link(adjacent, stemBase + ((edge + 1) % count), branchBase, Math.abs(end - at));
}
}
return new Network(xy, adjacent, edges);
}

private static Map<Integer, Double> cover(
Network network, double x, double z, double radius, double range, TrackReach.At provinceAt, int home) {
int count = network.xy.size();
boolean[] source = new boolean[count];
double radiusSquared = radius * radius;
for (int index = 0; index < count; index++) {
double dx = network.xy.get(index)[0] - x;
double dz = network.xy.get(index)[1] - z;
if (dx * dx + dz * dz <= radiusSquared) source[index] = true;
}
double[] distance = TrackReach.travel(network.adjacent, source, range);
Map<Integer, Double> out = new HashMap<>();
for (int index = 0; index < count; index++) {
if (!Double.isFinite(distance[index])) continue;
record(out, network.xy.get(index)[0], network.xy.get(index)[1], distance[index], range, provinceAt, home);
}
for (SplineEdge edge : network.edges) {
double from = distance[edge.from];
double to = distance[edge.to];
if (!Double.isFinite(from) && !Double.isFinite(to)) continue;
double weight = edge.weight;
int steps = weight <= 0 ? 0 : (int) Math.floor(weight / SAMPLE_SPACING);
for (int step = 0; step <= steps; step++) {
double travelled = weight <= 0 ? 0 : Math.min(weight, step * SAMPLE_SPACING);
recordAlong(out, edge, travelled, from, to, range, provinceAt, home);
}
recordAlong(out, edge, weight, from, to, range, provinceAt, home);
}
return Map.copyOf(out);
}

private static void recordAlong(
Map<Integer, Double> out, SplineEdge edge, double travelled,
double fromDistance, double toDistance, double range, TrackReach.At provinceAt, int home) {
double fromHere = Double.isFinite(fromDistance) ? fromDistance + travelled : Double.POSITIVE_INFINITY;
double fromThere = Double.isFinite(toDistance)
? toDistance + Math.max(0, edge.weight - travelled) : Double.POSITIVE_INFINITY;
double along = Math.min(fromHere, fromThere);
if (!Double.isFinite(along) || along > range) return;
double span = edge.endS - edge.startS;
double s = edge.weight <= EPS ? edge.startS : edge.startS + (travelled / edge.weight) * span;
TrackPose pose = edge.spline.sampleAt(s);
record(out, pose.x, pose.z, along, range, provinceAt, home);
}

private static void record(
Map<Integer, Double> out, double x, double z, double distance,
double range, TrackReach.At provinceAt, int home) {
if (!Double.isFinite(distance) || distance < 0 || distance > range) return;
int province = provinceAt.province(x, z);
if (province == 0 || province == home) return;
out.merge(province, distance, Math::min);
}

private static double startS(TrackSpline spline, int edge) {
List<TrackSample> samples = spline.getSamples();
return samples.get(Math.min(edge, samples.size() - 1)).s;
}

private static double endS(TrackSpline spline, int edge) {
List<TrackSample> samples = spline.getSamples();
if (edge < samples.size() - 1) return samples.get(edge + 1).s;
return spline.length();
}
}
Loading
Loading