Skip to content

Commit 883cf49

Browse files
ryanbarlow97claude
andcommitted
Build one shared graph of trade nodes and networks
Ports, airports and train stations become nodes. Ports join across SEA or WATER, airports join each other, and any two nodes join where track runs between them. Sea and rail edges remember the provinces between their ends. Connected nodes form networks, and the single largest one with at least two owners is the global network. Nothing reads the graph yet apart from /faction tradegraph. Needs VehicleFramework 2.10.0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 6a3df42 commit 883cf49

16 files changed

Lines changed: 1241 additions & 16 deletions

‎pom.xml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<denareconomy.version>0.2.4</denareconomy.version>
1616
<rpcharacters.version>2.0.1</rpcharacters.version>
1717
<tlibs.version>2.0.0</tlibs.version>
18-
<vehicleframework.version>2.8.0</vehicleframework.version>
18+
<vehicleframework.version>2.10.0</vehicleframework.version>
1919
<vfbuilders.version>2.1.0</vfbuilders.version>
2020
</properties>
2121

‎src/main/java/net/tfminecraft/simplefactions/SimpleFactions.java‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,11 @@ private boolean refreshTrackProvinces(Runnable recalculate) {
574574
}
575575
return net.tfminecraft.simplefactions.guild.hub.VehicleFrameworkTrackProvinces.sample(
576576
Cache.worldName, provinceGrid, provinces);
577-
}, recalculate,
577+
}, () -> {
578+
// Track changes the shared graph. Recalculation rebuilds it again before hub links.
579+
net.tfminecraft.simplefactions.guild.network.TradeGraph.refresh(provinceManager);
580+
recalculate.run();
581+
},
578582
message -> getLogger().warning("[SimpleFactions] " + message));
579583
}
580584

‎src/main/java/net/tfminecraft/simplefactions/guild/hub/HubNetwork.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import net.tfminecraft.simplefactions.Cache;
1313
import net.tfminecraft.simplefactions.SimpleFactions;
1414
import net.tfminecraft.simplefactions.guild.Guild;
15+
import net.tfminecraft.simplefactions.guild.network.TradeGraph;
1516
import net.tfminecraft.simplefactions.guild.GuildModifierOverride;
1617
import net.tfminecraft.simplefactions.guild.hub.HubTransport.Link;
1718
import net.tfminecraft.simplefactions.guild.hub.HubTransport.Mode;
@@ -60,6 +61,7 @@ public static List<Link> linksFor(Guild guild) {
6061

6162
/** Rebuilds every guild's connections. Server thread only. */
6263
public static void refresh(ProvinceManager provinces) {
64+
TradeGraph.refresh(provinces);
6365
Map<String, List<Link>> built = new HashMap<>();
6466
List<Guild> guilds = SupplyHubService.allGuilds();
6567
for (Guild guild : guilds) {
@@ -96,6 +98,7 @@ public static void refreshIfLive(ProvinceManager provinces) {
9698
*/
9799
public static void forgetRoutes() {
98100
railDistances.clear();
101+
TradeGraph.forgetRoutes();
99102
}
100103

101104
/**
Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,16 @@
11
package net.tfminecraft.simplefactions.guild.hub;
22

3-
import java.util.List;
43
import java.util.Map;
54
import java.util.Set;
65

76
import net.tfminecraft.simplefactions.map.ProvinceGrid;
8-
import net.tfminecraft.simplefactions.map.infra.TrackProvinceLookup;
9-
import net.tfminecraft.simplefactions.map.infra.TrackProvinceLookup.Point;
107
import net.tfminecraft.simplefactions.map.provinces.Province;
11-
import net.tfminecraft.vehicleframework.VehicleFramework;
12-
import net.tfminecraft.vehicleframework.tracks.TrackRegistry;
13-
import net.tfminecraft.vehicleframework.tracks.TrackSamplePoint;
148

15-
/** The only track sampler here that names VehicleFramework's sample point type. */
9+
/** Track infrastructure sampling uses the same VehicleFramework boundary as hub routes. */
1610
public final class VehicleFrameworkTrackProvinces {
1711
private VehicleFrameworkTrackProvinces() {}
1812

1913
public static Set<Integer> sample(String world, ProvinceGrid grid, Map<Integer, Province> provinces) {
20-
TrackRegistry tracks = VehicleFramework.getTrackRegistry();
21-
if (tracks == null || world == null) return Set.of();
22-
List<TrackSamplePoint> samples = tracks.sampleTrack(world, 8);
23-
return TrackProvinceLookup.collect(grid, samples.stream()
24-
.map(point -> new Point(point.x(), point.y(), point.z())).toList(), provinces);
14+
return VehicleFrameworkTracks.sample(world, grid, provinces);
2515
}
2616
}

‎src/main/java/net/tfminecraft/simplefactions/guild/hub/VehicleFrameworkTracks.java‎

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
package net.tfminecraft.simplefactions.guild.hub;
22

33
import java.util.OptionalDouble;
4+
import java.util.Map;
5+
import java.util.Optional;
6+
import java.util.Set;
47

8+
import org.bukkit.Bukkit;
9+
10+
import net.tfminecraft.simplefactions.guild.network.RailRoutes.Route;
511
import net.tfminecraft.simplefactions.installation.Installation;
612
import net.tfminecraft.simplefactions.loaders.InstallationConfigLoader;
13+
import net.tfminecraft.simplefactions.map.ProvinceGrid;
14+
import net.tfminecraft.simplefactions.map.infra.TrackProvinceLookup;
15+
import net.tfminecraft.simplefactions.map.infra.TrackProvinceLookup.Point;
16+
import net.tfminecraft.simplefactions.map.provinces.Province;
717
import net.tfminecraft.vehicleframework.VehicleFramework;
818
import net.tfminecraft.vehicleframework.tracks.TrackRegistry;
919

1020
/**
1121
* The only class here that names VehicleFramework types, so it is loaded only once
12-
* {@link HubNetwork} has checked that the plugin is enabled.
22+
* the caller has checked that the plugin is enabled, or through the guarded route method.
1323
*/
14-
final class VehicleFrameworkTracks {
24+
public final class VehicleFrameworkTracks {
1525
private VehicleFrameworkTracks() {
1626
}
1727

@@ -29,4 +39,27 @@ static OptionalDouble distance(String world, Installation from, Installation to)
2939
to.getCenterZ(),
3040
InstallationConfigLoader.getRadius(to.getKind()));
3141
}
42+
43+
/** Empty when VehicleFramework is missing or too old to return route points. */
44+
public static Optional<Route> route(String world, Installation from, Installation to) {
45+
try {
46+
if (!Bukkit.getPluginManager().isPluginEnabled("VehicleFramework")) return Optional.empty();
47+
TrackRegistry tracks = VehicleFramework.getTrackRegistry();
48+
if (tracks == null || world == null) return Optional.empty();
49+
return tracks.shortestRoute(world,
50+
from.getCenterX(), from.getCenterZ(), InstallationConfigLoader.getRadius(from.getKind()),
51+
to.getCenterX(), to.getCenterZ(), InstallationConfigLoader.getRadius(to.getKind()), 8)
52+
.map(route -> new Route(route.length(), route.points().stream()
53+
.map(point -> new Point(point.x(), point.y(), point.z())).toList()));
54+
} catch (RuntimeException | LinkageError e) {
55+
return Optional.empty();
56+
}
57+
}
58+
59+
static Set<Integer> sample(String world, ProvinceGrid grid, Map<Integer, Province> provinces) {
60+
TrackRegistry tracks = VehicleFramework.getTrackRegistry();
61+
if (tracks == null || world == null) return Set.of();
62+
return TrackProvinceLookup.collect(grid, tracks.sampleTrack(world, 8).stream()
63+
.map(point -> new Point(point.x(), point.y(), point.z())).toList(), provinces);
64+
}
3265
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package net.tfminecraft.simplefactions.guild.network;
2+
3+
import java.util.ArrayList;
4+
import java.util.HashMap;
5+
import java.util.IdentityHashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
import java.util.Optional;
9+
10+
import org.bukkit.Bukkit;
11+
12+
import net.tfminecraft.simplefactions.Cache;
13+
import net.tfminecraft.simplefactions.SimpleFactions;
14+
import net.tfminecraft.simplefactions.guild.hub.VehicleFrameworkTracks;
15+
import net.tfminecraft.simplefactions.guild.network.RailRoutes.Route;
16+
import net.tfminecraft.simplefactions.guild.network.TradeGraphBuilder.ProvinceData;
17+
import net.tfminecraft.simplefactions.guild.network.TradeGraphBuilder.Site;
18+
import net.tfminecraft.simplefactions.installation.Installation;
19+
import net.tfminecraft.simplefactions.installation.InstallationKind;
20+
import net.tfminecraft.simplefactions.loaders.InstallationConfigLoader;
21+
import net.tfminecraft.simplefactions.managers.FactionManager;
22+
import net.tfminecraft.simplefactions.managers.ProvinceManager;
23+
import net.tfminecraft.simplefactions.map.ProvinceGrid;
24+
import net.tfminecraft.simplefactions.map.provinces.Province;
25+
import net.tfminecraft.simplefactions.objects.Faction;
26+
27+
/** Takes the server-thread data needed by the pure builder. */
28+
final class LiveTradeGraph {
29+
private static final RailRouteCache routes = new RailRouteCache(System::currentTimeMillis);
30+
31+
private LiveTradeGraph() { }
32+
33+
static TradeGraph build(ProvinceManager provinces) {
34+
List<Site> sites = new ArrayList<>();
35+
Map<Installation, String> identities = new IdentityHashMap<>();
36+
if (FactionManager.factions != null) {
37+
for (Faction faction : FactionManager.factions) {
38+
if (faction == null || faction.getId() == null || faction.getInstallationHandler() == null) continue;
39+
// Pending construction is separate from getAll(), as in SupplyHubService.findInstallation.
40+
for (Installation installation : faction.getInstallationHandler().getAll()) {
41+
if (installation == null || installation.getKind() == InstallationKind.FORT) continue;
42+
sites.add(new Site(faction.getId(), installation, InstallationConfigLoader.getHubSlots(
43+
installation.getKind(), installation.getLevel()), true));
44+
identities.put(installation,
45+
faction.getId() + ":" + installation.getProvince() + ":" + installation.getId());
46+
}
47+
}
48+
}
49+
Map<Integer, ProvinceData> map = new HashMap<>();
50+
for (Province province : provinces.getProvinces()) {
51+
if (province.isValid()) {
52+
map.put(province.getId(), new ProvinceData(province.getTerrain(), province.getNeighbours()));
53+
}
54+
}
55+
SimpleFactions plugin = SimpleFactions.getInstance();
56+
ProvinceGrid grid = plugin == null ? null : plugin.getProvinceGrid();
57+
String world = Cache.worldName;
58+
return TradeGraphBuilder.build(sites, map, (from, to) -> {
59+
String left = identities.get(from);
60+
String right = identities.get(to);
61+
if (left == null || right == null) return Optional.empty();
62+
return routes.route(left, right, () -> trackRoute(world, from, to));
63+
}, point -> grid == null ? 0 : grid.getAt(
64+
(int) Math.floor(point.x()), (int) Math.floor(point.z())));
65+
}
66+
67+
/**
68+
* The plugin check sits outside {@link VehicleFrameworkTracks}, matching hub distances.
69+
* A missing or older plugin must not fail the refresh.
70+
*/
71+
private static Optional<Route> trackRoute(String world, Installation from, Installation to) {
72+
try {
73+
if (!Bukkit.getPluginManager().isPluginEnabled("VehicleFramework")) return Optional.empty();
74+
return VehicleFrameworkTracks.route(world, from, to);
75+
} catch (RuntimeException | LinkageError e) {
76+
return Optional.empty();
77+
}
78+
}
79+
80+
static void forgetRoutes() {
81+
routes.forget();
82+
}
83+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package net.tfminecraft.simplefactions.guild.network;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
import java.util.Optional;
6+
import java.util.function.LongSupplier;
7+
import java.util.function.Supplier;
8+
9+
import net.tfminecraft.simplefactions.guild.network.RailRoutes.Route;
10+
11+
/** Hits last until forgotten; misses are tried again after thirty seconds. */
12+
final class RailRouteCache {
13+
private record Key(String first, String second) { }
14+
private record Cached(Optional<Route> route, long expiresAt) { }
15+
16+
private final Map<Key, Cached> routes = new HashMap<>();
17+
private final LongSupplier clock;
18+
19+
RailRouteCache(LongSupplier clock) {
20+
this.clock = clock;
21+
}
22+
23+
Optional<Route> route(String from, String to, Supplier<Optional<Route>> source) {
24+
if (from == null || to == null) return Optional.empty();
25+
boolean reverse = from.compareTo(to) > 0;
26+
Key key = reverse ? new Key(to, from) : new Key(from, to);
27+
long now = clock.getAsLong();
28+
Cached known = routes.get(key);
29+
if (known != null && (known.route().isPresent() || known.expiresAt() > now)) {
30+
return reverse ? known.route().map(Route::reversed) : known.route();
31+
}
32+
Optional<Route> measured = source.get();
33+
routes.put(key, new Cached(reverse ? measured.map(Route::reversed) : measured,
34+
measured.isPresent() ? Long.MAX_VALUE : now + 30_000L));
35+
return measured;
36+
}
37+
38+
void forget() {
39+
routes.clear();
40+
}
41+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package net.tfminecraft.simplefactions.guild.network;
2+
3+
import java.util.List;
4+
import java.util.Optional;
5+
6+
import net.tfminecraft.simplefactions.installation.Installation;
7+
import net.tfminecraft.simplefactions.map.infra.TrackProvinceLookup.Point;
8+
9+
/** Routes contain points in order from the first installation to the second. */
10+
@FunctionalInterface
11+
public interface RailRoutes {
12+
Optional<Route> route(Installation from, Installation to);
13+
14+
record Route(double length, List<Point> points) {
15+
public Route {
16+
points = List.copyOf(points);
17+
}
18+
19+
public Route reversed() {
20+
return new Route(length, points.reversed());
21+
}
22+
}
23+
}

0 commit comments

Comments
 (0)