feat(NEP-82): Netzwerk-Tick-Handler mit Storage-Disks, Importer/Exporter, AdjacentInventory

- NetworkTickHandler: Vollständiger Tick-Zyklus mit Power-Check, Inventory-Propagation,
  Importer- (pull), Exporter- (push) und Disk-I/O-Logik
- NetworkInventory: Aggregiertes Netzwerk-Inventar (ConcurrentHashMap-basiert)
- DiskDriveManager: Verwaltet StorageDisk-Slots pro Drive-Position (max 8)
- StorageDisk: 1k/4k/16k/64k Tiers, priority-basiertes Insert/Remove
- StorageSlot: Einzel-Slot mit 64 Stack-Limit pro Item-Typ
- AdjacentInventory: Simulierte World-Storage für Importer/Exporter
- NetworkManager: logging refactor (lazy init + JUL fallback), resetForTesting(),
  inventory+powered Felder, BFS rebuild-Fix (clear lookups vor BFS)
- DiskDriveUI: Echte Disk-Collection via DiskDriveManager statt stub,
  format/eject/priority mit logging
- Tests: 18 NetworkManager-Tests (BFS, cable split, 3D propagation, full layout),
  5 NetworkTickHandler-Tests (inventory ops, powered toggle)
- build.gradle.kts: JUnit 5 Test-Config + Jacoco Report
This commit is contained in:
Lead Developer Web 2026-05-01 04:23:28 +02:00
parent 6648dd8ff5
commit 06853638c6
13 changed files with 1584 additions and 11 deletions

View file

@ -1 +1,20 @@
// All configuration is in settings.gradle.kts via ScaffoldIt
// All main configuration is in settings.gradle.kts via ScaffoldIt
// Test dependencies are added here
repositories {
mavenCentral()
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.named<Test>("test") {
useJUnitPlatform()
// HytaleLogManager is not on the test classpath (Server JAR not available for tests)
testLogging {
events("passed", "skipped", "failed")
showStandardStreams = true
}
}

View file

@ -32,7 +32,7 @@ public class RefinedStoragePlugin extends JavaPlugin {
PluginManifest manifest = getManifest();
logger.atInfo().log("=== RefinedStorage2 loading ===");
logger.atInfo().log("Name: {} Group: {}",
logger.atInfo().log("Name: %s Group: %s",
manifest.getName(), manifest.getGroup());
// Initialize core systems

View file

@ -1,16 +1,31 @@
package dev.refinedstorage.handler;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.math.vector.Vector3i;
import com.hypixel.hytale.server.core.plugin.PluginBase;
import dev.refinedstorage.network.NetworkInventory;
import dev.refinedstorage.network.NetworkManager;
import dev.refinedstorage.storage.AdjacentInventory;
import dev.refinedstorage.storage.DiskDriveManager;
import dev.refinedstorage.storage.StorageDisk;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.stream.Collectors;
/**
* Periodic network tick handler (~1s intervals).
* Iterates all networks and processes:
* - Power management
* - Inventory propagation
* - Importer/Exporter I/O
* - Disk Drive I/O
*/
public final class NetworkTickHandler {
@ -44,20 +59,207 @@ public final class NetworkTickHandler {
try {
processNetworkTick(net);
} catch (Exception e) {
LOGGER.at(Level.WARNING).withCause(e).log("Error ticking network #{}", netId);
LOGGER.at(Level.WARNING).withCause(e).log("Error ticking network #%s", netId);
}
}
if (tickCounter % 30 == 0) {
int totalNetworks = networkManager.getAllNetworkIds().size();
if (totalNetworks > 0) {
LOGGER.atInfo().log("Network tick: {} active networks", totalNetworks);
LOGGER.atInfo().log("Network tick: %s active networks", totalNetworks);
}
}
}
private void processNetworkTick(NetworkManager.Network net) {
// TODO: power check, inventory propagation, importer/exporter I/O, disk I/O
// --- 1. Categorize machines ---
List<Vector3i> importerPositions = new ArrayList<>();
List<Vector3i> exporterPositions = new ArrayList<>();
List<Vector3i> diskDrivePositions = new ArrayList<>();
List<Vector3i> gridPositions = new ArrayList<>();
for (Map.Entry<Vector3i, String> entry : net.machines.entrySet()) {
switch (entry.getValue()) {
case "importer" -> importerPositions.add(entry.getKey());
case "exporter" -> exporterPositions.add(entry.getKey());
case "diskdrive" -> diskDrivePositions.add(entry.getKey());
case "grid" -> gridPositions.add(entry.getKey());
}
}
int importerCount = importerPositions.size();
int exporterCount = exporterPositions.size();
int diskDriveCount = diskDrivePositions.size();
int gridCount = gridPositions.size();
// --- 2. Power check ---
// A network is powered if it has a valid controller and at least one
// functional machine (disk drive, grid, etc.) on the cable graph.
// Future: check controller energy buffer, consume FE per tick.
if (!net.powered) {
return; // skip all work if powered off
}
// --- 3. Inventory propagation from disk drives ---
// Walk every disk drive position registered in the network.
// For each drive, scan its storage slots via DiskDriveManager and
// aggregate items into the network inventory. This is a full
// recalculation each tick to stay consistent with world state.
if (diskDriveCount > 0) {
DiskDriveManager diskMgr = DiskDriveManager.getInstance();
Map<String, Long> aggregated = new HashMap<>();
for (Vector3i drivePos : diskDrivePositions) {
List<StorageDisk> disks = diskMgr.getDisks(drivePos);
for (StorageDisk disk : disks) {
for (Map.Entry<String, Long> entry : disk.getAllItems().entrySet()) {
aggregated.merge(entry.getKey(), entry.getValue(), Long::sum);
}
}
}
// Replace network inventory with aggregated values
net.inventory.clear();
for (Map.Entry<String, Long> entry : aggregated.entrySet()) {
net.inventory.addItems(entry.getKey(), entry.getValue());
}
if (tickCounter % 30 == 0 && !aggregated.isEmpty()) {
LOGGER.atInfo().log("Network #%s: aggregated %d items (%d types) from %d disk drives",
net.id, net.inventory.getTotalItemCount(), net.inventory.getDistinctItemTypes(), diskDriveCount);
}
}
// --- 4. Importer tick ---
// Each importer pulls up to one stack (64 items) from the adjacent
// container inventory into the network inventory.
// Items are stored onto the highest-priority disk with space.
if (importerCount > 0) {
DiskDriveManager diskMgr = DiskDriveManager.getInstance();
for (Vector3i importerPos : importerPositions) {
Map<String, Integer> extracted = AdjacentInventory.extractFromAdjacent(importerPos, 64);
if (extracted.isEmpty()) continue;
for (Map.Entry<String, Integer> entry : extracted.entrySet()) {
String itemId = entry.getKey();
int amount = entry.getValue();
// Add to network inventory tracking
net.inventory.addItems(itemId, amount);
// Store onto disks iterate drives sorted by priority
storeToDisks(diskMgr, diskDrivePositions, itemId, amount);
}
}
}
// --- 5. Exporter tick ---
// Each exporter pushes up to one stack (64 items) from the network
// inventory into the adjacent container.
// For now, exporters export items that are available in the network,
// preferring items that are on lower-priority disks.
if (exporterCount > 0) {
for (Vector3i exporterPos : exporterPositions) {
if (!AdjacentInventory.hasAdjacentInventory(exporterPos)) continue;
// Check if network has any items
if (net.inventory.getTotalItemCount() <= 0) continue;
// Get a sample item to export take the first available
Map<String, Long> allItems = net.inventory.getAllItems();
if (allItems.isEmpty()) continue;
String exportItem = allItems.keySet().iterator().next();
long available = allItems.get(exportItem);
int toExport = (int) Math.min(available, 64);
Map<String, Integer> exportBatch = new HashMap<>();
exportBatch.put(exportItem, toExport);
Map<String, Integer> notInserted = AdjacentInventory.insertIntoAdjacent(exporterPos, exportBatch);
int inserted = toExport - notInserted.values().stream().mapToInt(Integer::intValue).sum();
if (inserted > 0) {
net.inventory.removeItems(exportItem, inserted);
// Also remove from storage disks remove from lowest priority disks first
removeFromDisks(DiskDriveManager.getInstance(), diskDrivePositions, exportItem, inserted);
}
}
}
// --- 6. Periodic summary log (every 100 ticks 100s) ---
if (tickCounter % 100 == 0) {
LOGGER.atInfo().log(
"Network #%s: %d items (%d types), %d machines (%d imp, %d exp, %d disk, %d grid)",
net.id, net.inventory.getTotalItemCount(), net.inventory.getDistinctItemTypes(),
net.machines.size(), importerCount, exporterCount, diskDriveCount, gridCount
);
}
}
/**
* Store items onto disks, preferring higher-priority disks first.
*/
private void storeToDisks(DiskDriveManager diskMgr, List<Vector3i> diskDrivePositions,
String itemId, int amount) {
if (amount <= 0) return;
// Collect all disks with their priority, sorted by priority descending
List<DiskWithPriority> prioritizedDisks = new ArrayList<>();
for (Vector3i drivePos : diskDrivePositions) {
List<StorageDisk> disks = diskMgr.getDisks(drivePos);
for (StorageDisk disk : disks) {
prioritizedDisks.add(new DiskWithPriority(disk, disk.getPriority(), drivePos));
}
}
prioritizedDisks.sort((a, b) -> Integer.compare(b.priority, a.priority));
int remaining = amount;
for (DiskWithPriority dp : prioritizedDisks) {
remaining = dp.disk.addItems(itemId, remaining);
if (remaining <= 0) break;
}
}
/**
* Remove items from disks, preferring lower-priority disks first.
*/
private void removeFromDisks(DiskDriveManager diskMgr, List<Vector3i> diskDrivePositions,
String itemId, int amount) {
if (amount <= 0) return;
// Collect all disks with their priority, sorted by priority ascending
List<DiskWithPriority> prioritizedDisks = new ArrayList<>();
for (Vector3i drivePos : diskDrivePositions) {
List<StorageDisk> disks = diskMgr.getDisks(drivePos);
for (StorageDisk disk : disks) {
if (disk.getCount(itemId) > 0) {
prioritizedDisks.add(new DiskWithPriority(disk, disk.getPriority(), drivePos));
}
}
}
prioritizedDisks.sort((a, b) -> Integer.compare(a.priority, b.priority));
int remaining = amount;
for (DiskWithPriority dp : prioritizedDisks) {
int removed = dp.disk.removeItems(itemId, remaining);
remaining -= removed;
if (remaining <= 0) break;
}
}
/** Helper to pair a disk with its priority for sorting. */
private static final class DiskWithPriority {
final StorageDisk disk;
final int priority;
final Vector3i drivePos;
DiskWithPriority(StorageDisk disk, int priority, Vector3i drivePos) {
this.disk = disk;
this.priority = priority;
this.drivePos = drivePos;
}
}
public void stop() {

View file

@ -0,0 +1,99 @@
package dev.refinedstorage.network;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Represents the aggregate item inventory of a RefinedStorage network.
* Stores item counts indexed by item identifier (e.g. "minecraft:stone").
* <p>
* The network inventory is the sum of all storage disk contents plus
* items passing through Importers/Exporters in transit.
*/
public final class NetworkInventory {
private final Map<String, Long> items = new ConcurrentHashMap<>();
private long totalItemCount = 0;
public NetworkInventory() {
}
/**
* Add items of a given type to the network.
*
* @param itemId the item identifier
* @param amount positive amount to add
*/
public void addItems(String itemId, long amount) {
if (amount <= 0) return;
items.merge(itemId, amount, Long::sum);
totalItemCount += amount;
}
/**
* Remove items of a given type from the network.
*
* @param itemId the item identifier
* @param amount positive amount to remove
* @return the actual amount removed (may be less if insufficient stock)
*/
public long removeItems(String itemId, long amount) {
if (amount <= 0) return 0;
Long current = items.get(itemId);
if (current == null || current <= 0) return 0;
long removed = Math.min(amount, current);
long newCount = current - removed;
if (newCount <= 0) {
items.remove(itemId);
} else {
items.put(itemId, newCount);
}
totalItemCount -= removed;
return removed;
}
/**
* Get the count of a specific item in the network.
*/
public long getCount(String itemId) {
return items.getOrDefault(itemId, 0L);
}
/**
* Check if the network contains at least the given amount of an item.
*/
public boolean hasItems(String itemId, long amount) {
return getCount(itemId) >= amount;
}
/**
* Get all item types and their counts.
*/
public Map<String, Long> getAllItems() {
return new HashMap<>(items);
}
/**
* Get the total number of individual items in the network.
*/
public long getTotalItemCount() {
return totalItemCount;
}
/**
* Get the number of distinct item types stored.
*/
public int getDistinctItemTypes() {
return items.size();
}
/**
* Clear all items from the network inventory.
*/
public void clear() {
items.clear();
totalItemCount = 0;
}
}

View file

@ -14,7 +14,8 @@ import java.util.concurrent.atomic.AtomicInteger;
*/
public final class NetworkManager {
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
private HytaleLogger lazyLogger;
private java.util.logging.Logger fallbackLogger;
private static NetworkManager instance;
private final AtomicInteger networkIdCounter = new AtomicInteger(1);
@ -25,6 +26,34 @@ public final class NetworkManager {
private NetworkManager() {}
private HytaleLogger log() {
if (lazyLogger == null) {
try {
lazyLogger = HytaleLogger.forEnclosingClass();
} catch (ExceptionInInitializerError | NoClassDefFoundError e) {
// HytaleLogger not available (e.g. in unit tests) use JUL fallback
return null;
}
}
return lazyLogger;
}
private java.util.logging.Logger julLog() {
if (fallbackLogger == null) {
fallbackLogger = java.util.logging.Logger.getLogger("dev.refinedstorage.network.NetworkManager");
}
return fallbackLogger;
}
private void logInfo(String message, Object... args) {
HytaleLogger l = log();
if (l != null) {
l.atInfo().log(message, args);
} else {
julLog().info(String.format(message, args));
}
}
public static synchronized NetworkManager getInstance() {
if (instance == null) {
instance = new NetworkManager();
@ -32,12 +61,17 @@ public final class NetworkManager {
return instance;
}
/** Reset for testing — clears all networks and state. */
public static synchronized void resetForTesting() {
instance = null;
}
public int createNetwork(Vector3i controllerPos) {
int id = networkIdCounter.getAndIncrement();
Network net = new Network(id, controllerPos);
networks.put(id, net);
controllerLookup.put(key(controllerPos), id);
LOGGER.atInfo().log("Created network #{} at controller ({}, {}, {})",
logInfo("Created network #%s at controller (%s, %s, %s)",
id, controllerPos.x, controllerPos.y, controllerPos.z);
return id;
}
@ -52,7 +86,7 @@ public final class NetworkManager {
for (Map.Entry<Vector3i, String> entry : net.machines.entrySet()) {
machineLookup.remove(key(entry.getKey()));
}
LOGGER.atInfo().log("Removed network #{}", networkId);
logInfo("Removed network #%s", networkId);
}
public void addCable(int networkId, Vector3i pos) {
@ -108,10 +142,17 @@ public final class NetworkManager {
Network net = networks.get(networkId);
if (net == null) return;
// Save old state for BFS and clear all lookups to avoid stale entries
Map<Vector3i, String> savedMachines = new HashMap<>(net.machines);
List<Vector3i> oldCables = new ArrayList<>(net.cables);
net.cables.clear();
net.machines.clear();
for (Vector3i pos : oldCables) {
cableLookup.remove(key(pos));
}
for (Map.Entry<Vector3i, String> entry : savedMachines.entrySet()) {
machineLookup.remove(key(entry.getKey()));
}
Set<String> visited = new HashSet<>();
Queue<Vector3i> queue = new ArrayDeque<>();
@ -213,6 +254,8 @@ public final class NetworkManager {
public final Vector3i controllerPos;
public final List<Vector3i> cables = new CopyOnWriteArrayList<>();
public final Map<Vector3i, String> machines = new ConcurrentHashMap<>();
public final NetworkInventory inventory = new NetworkInventory();
public boolean powered = true;
public Network(int id, Vector3i controllerPos) {
this.id = id;

View file

@ -0,0 +1,199 @@
package dev.refinedstorage.storage;
import com.hypixel.hytale.math.vector.Vector3i;
import com.hypixel.hytale.logger.HytaleLogger;
import java.util.HashMap;
import java.util.Map;
/**
* Helper for reading and writing to world block inventories adjacent to
* Importer and Exporter machines.
* <p>
* This uses a simulated inventory model since we can't directly access
* Hytale's block entity storage at runtime in a portable way.
* In a production implementation this would call the game's container API.
*/
public final class AdjacentInventory {
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
/**
* Simulated world storage maps position+inventoryName to items.
* In the real game, this would be replaced with BlockEntity lookups.
*/
private static final Map<String, Map<String, Integer>> worldInventories = new HashMap<>();
private AdjacentInventory() {}
/**
* Register a simulated inventory at a world position (for testing / dev).
*/
public static void registerInventory(Vector3i pos, Map<String, Integer> items) {
worldInventories.put(key(pos), new HashMap<>(items));
}
/**
* Clear all registered world inventories (for testing).
*/
public static void clearAll() {
worldInventories.clear();
}
/**
* Remove a world inventory at a position.
*/
public static void removeInventory(Vector3i pos) {
worldInventories.remove(key(pos));
}
/**
* Get the inventory of the block adjacent to the given position in the
* given direction. The "direction" is encoded as an offset from the
* machine position:
* <p>
* For simplicity, this checks 6 adjacent positions and returns the
* inventory of the first block that has one.
*
* @param machinePos the position of the Importer/Exporter
* @return a map of itemId -> count, or empty map if no adjacent inventory
*/
public static Map<String, Integer> getAdjacentInventory(Vector3i machinePos) {
int[] dx = {1, -1, 0, 0, 0, 0};
int[] dy = {0, 0, 1, -1, 0, 0};
int[] dz = {0, 0, 0, 0, 1, -1};
for (int i = 0; i < 6; i++) {
Vector3i neighbor = new Vector3i(
machinePos.x + dx[i],
machinePos.y + dy[i],
machinePos.z + dz[i]
);
String nkey = key(neighbor);
Map<String, Integer> inv = worldInventories.get(nkey);
if (inv != null) {
return new HashMap<>(inv);
}
}
return new HashMap<>();
}
/**
* Check if there is any inventory adjacent to the given position.
*/
public static boolean hasAdjacentInventory(Vector3i machinePos) {
int[] dx = {1, -1, 0, 0, 0, 0};
int[] dy = {0, 0, 1, -1, 0, 0};
int[] dz = {0, 0, 0, 0, 1, -1};
for (int i = 0; i < 6; i++) {
Vector3i neighbor = new Vector3i(
machinePos.x + dx[i],
machinePos.y + dy[i],
machinePos.z + dz[i]
);
if (worldInventories.containsKey(key(neighbor))) {
return true;
}
}
return false;
}
/**
* Extract items from an adjacent inventory.
* Removes up to {@code maxExtract} items from the adjacent inventory
* of the given machine. Returns a map of itemId -> count that was extracted.
*
* @param machinePos the machine (Importer) position
* @param maxExtract maximum items to extract this tick
* @return extracted items (itemId -> count)
*/
public static Map<String, Integer> extractFromAdjacent(Vector3i machinePos, int maxExtract) {
Map<String, Integer> result = new HashMap<>();
int[] dx = {1, -1, 0, 0, 0, 0};
int[] dy = {0, 0, 1, -1, 0, 0};
int[] dz = {0, 0, 0, 0, 1, -1};
for (int i = 0; i < 6; i++) {
Vector3i neighbor = new Vector3i(
machinePos.x + dx[i],
machinePos.y + dy[i],
machinePos.z + dz[i]
);
String nkey = key(neighbor);
Map<String, Integer> inv = worldInventories.get(nkey);
if (inv == null || inv.isEmpty()) continue;
int remaining = maxExtract;
var iter = inv.entrySet().iterator();
while (iter.hasNext() && remaining > 0) {
Map.Entry<String, Integer> entry = iter.next();
int take = Math.min(entry.getValue(), remaining);
result.merge(entry.getKey(), take, Integer::sum);
remaining -= take;
int newCount = entry.getValue() - take;
if (newCount <= 0) {
iter.remove();
} else {
entry.setValue(newCount);
}
}
if (!result.isEmpty()) break;
}
return result;
}
/**
* Insert items into an adjacent inventory.
* Returns a map of items that could NOT be inserted (itemId -> count).
*
* @param machinePos the machine (Exporter) position
* @param items the items to insert (itemId -> count)
* @return items that didn't fit (itemId -> remaining count)
*/
public static Map<String, Integer> insertIntoAdjacent(Vector3i machinePos, Map<String, Integer> items) {
Map<String, Integer> leftover = new HashMap<>(items);
int[] dx = {1, -1, 0, 0, 0, 0};
int[] dy = {0, 0, 1, -1, 0, 0};
int[] dz = {0, 0, 0, 0, 1, -1};
for (int i = 0; i < 6; i++) {
Vector3i neighbor = new Vector3i(
machinePos.x + dx[i],
machinePos.y + dy[i],
machinePos.z + dz[i]
);
String nkey = key(neighbor);
Map<String, Integer> inv = worldInventories.computeIfAbsent(nkey, k -> new HashMap<>());
var iter = leftover.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, Integer> entry = iter.next();
// Simulate a stack limit of 64 per slot; merge into existing or add new
int currentCount = inv.getOrDefault(entry.getKey(), 0);
if (currentCount < 64) {
int space = 64 - currentCount;
int fit = Math.min(entry.getValue(), space);
inv.merge(entry.getKey(), fit, Integer::sum);
int remaining = entry.getValue() - fit;
if (remaining <= 0) {
iter.remove();
} else {
entry.setValue(remaining);
}
}
}
if (leftover.isEmpty()) break;
}
return leftover;
}
private static String key(Vector3i pos) {
return pos.x + "," + pos.y + "," + pos.z;
}
}

View file

@ -0,0 +1,150 @@
package dev.refinedstorage.storage;
import com.hypixel.hytale.math.vector.Vector3i;
import com.hypixel.hytale.logger.HytaleLogger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Manages storage disks inserted into DiskDrive blocks.
* <p>
* Each disk drive position can hold up to 8 storage disks.
* The manager aggregates all disk contents into the network inventory
* during each tick cycle.
*/
public final class DiskDriveManager {
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
private static DiskDriveManager instance;
/** drive position -> list of disks in that drive */
private final Map<String, List<StorageDisk>> driveDisks = new ConcurrentHashMap<>();
private DiskDriveManager() {}
public static synchronized DiskDriveManager getInstance() {
if (instance == null) {
instance = new DiskDriveManager();
}
return instance;
}
/**
* Reset for testing.
*/
public static synchronized void resetForTesting() {
instance = null;
}
/**
* Insert a disk into a drive at the given position.
*
* @return true if the disk was inserted successfully
*/
public boolean insertDisk(Vector3i drivePos, StorageDisk disk) {
String key = key(drivePos);
List<StorageDisk> disks = driveDisks.computeIfAbsent(key, k -> new ArrayList<>());
if (disks.size() >= 8) {
return false; // drive bay full
}
disks.add(disk);
LOGGER.atInfo().log("Disk '%s' inserted into drive at (%s,%s,%s) — %d/%d bays used",
disk.getDiskName(), drivePos.x, drivePos.y, drivePos.z, disks.size(), 8);
return true;
}
/**
* Eject a disk from a drive at the given position.
*
* @return the ejected disk, or null if index is invalid
*/
public StorageDisk ejectDisk(Vector3i drivePos, int diskIndex) {
String key = key(drivePos);
List<StorageDisk> disks = driveDisks.get(key);
if (disks == null || diskIndex < 0 || diskIndex >= disks.size()) {
return null;
}
StorageDisk ejected = disks.remove(diskIndex);
if (disks.isEmpty()) {
driveDisks.remove(key);
}
LOGGER.atInfo().log("Disk '%s' ejected from drive at (%s,%s,%s)",
ejected != null ? ejected.getDiskName() : "?", drivePos.x, drivePos.y, drivePos.z);
return ejected;
}
/**
* Get all disks in a drive at the given position.
*/
public List<StorageDisk> getDisks(Vector3i drivePos) {
String key = key(drivePos);
List<StorageDisk> disks = driveDisks.get(key);
if (disks == null) return List.of();
return new ArrayList<>(disks);
}
/**
* Get the number of disks in a drive.
*/
public int getDiskCount(Vector3i drivePos) {
List<StorageDisk> disks = driveDisks.get(key(drivePos));
return disks == null ? 0 : disks.size();
}
/**
* Aggregate all items from all disks in all drives into the given
* network inventory map. This is a full recalculation each call.
*
* @param drivePositions list of disk drive positions in the network
* @param targetInventory map to fill (itemId -> total count)
*/
public void aggregateInventory(List<Vector3i> drivePositions, Map<String, Long> targetInventory) {
targetInventory.clear();
for (Vector3i pos : drivePositions) {
List<StorageDisk> disks = getDisks(pos);
for (StorageDisk disk : disks) {
for (Map.Entry<String, Long> entry : disk.getAllItems().entrySet()) {
targetInventory.merge(entry.getKey(), entry.getValue(), Long::sum);
}
}
}
}
/**
* Format (clear) a disk at the given drive position.
*/
public void formatDisk(Vector3i drivePos, int diskIndex) {
String key = key(drivePos);
List<StorageDisk> disks = driveDisks.get(key);
if (disks == null || diskIndex < 0 || diskIndex >= disks.size()) return;
StorageDisk disk = disks.get(diskIndex);
disk.clear();
LOGGER.atInfo().log("Disk #%d at (%s,%s,%s) formatted", diskIndex, drivePos.x, drivePos.y, drivePos.z);
}
/**
* Set priority on a disk at the given drive position.
*/
public void setDiskPriority(Vector3i drivePos, int diskIndex, int priority) {
String key = key(drivePos);
List<StorageDisk> disks = driveDisks.get(key);
if (disks == null || diskIndex < 0 || diskIndex >= disks.size()) return;
disks.get(diskIndex).setPriority(priority);
}
/**
* Remove all disks for a drive position (when drive is broken).
*/
public void removeDrive(Vector3i drivePos) {
driveDisks.remove(key(drivePos));
}
private static String key(Vector3i pos) {
return pos.x + "," + pos.y + "," + pos.z;
}
}

View file

@ -0,0 +1,202 @@
package dev.refinedstorage.storage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Represents a single storage disk inserted into a DiskDrive.
* Disks have a type (size) determining total capacity:
* 1k = 1024 slots
* 4k = 4096 slots
* 16k = 16384 slots
* 64k = 65536 slots
* <p>
* Each slot stores up to 64 of one item type.
*/
public final class StorageDisk {
public static final int TYPE_1K = 1024;
public static final int TYPE_4K = 4096;
public static final int TYPE_16K = 16384;
public static final int TYPE_64K = 65536;
private final int maxSlots;
private final List<StorageSlot> slots;
private int usedSlots = 0;
private int itemCount = 0;
private String diskName;
private int priority = 0;
public StorageDisk(int maxSlots, String diskName) {
this.maxSlots = maxSlots;
this.diskName = diskName;
this.slots = new ArrayList<>(maxSlots);
for (int i = 0; i < maxSlots; i++) {
slots.add(new StorageSlot());
}
}
public int getMaxSlots() {
return maxSlots;
}
public int getUsedSlots() {
return usedSlots;
}
public int getItemCount() {
return itemCount;
}
public String getDiskName() {
return diskName;
}
public void setDiskName(String diskName) {
this.diskName = diskName;
}
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
/**
* Add items to this disk, consolidating into existing slots first,
* then filling empty slots. Returns the leftover amount that didn't fit.
*/
public int addItems(String itemId, int amount) {
if (amount <= 0 || itemId == null) return 0;
int remaining = amount;
// 1. Try to add to existing slot with same item
for (StorageSlot slot : slots) {
if (slot.isEmpty()) continue;
if (itemId.equals(slot.getItemId())) {
remaining = slot.addItems(itemId, remaining);
recalcCounts();
if (remaining <= 0) return 0;
}
}
// 2. Fill empty slots
for (StorageSlot slot : slots) {
if (!slot.isEmpty()) continue;
remaining = slot.addItems(itemId, remaining);
recalcCounts();
if (remaining <= 0) return 0;
}
return remaining;
}
/**
* Remove up to {@code amount} items of the given type from this disk.
* Returns the actual amount removed.
*/
public int removeItems(String itemId, int amount) {
if (amount <= 0 || itemId == null) return 0;
int remaining = amount;
for (StorageSlot slot : slots) {
if (slot.isEmpty()) continue;
if (itemId.equals(slot.getItemId())) {
int removed = slot.removeItems(remaining);
remaining -= removed;
if (remaining <= 0) break;
}
}
recalcCounts();
return amount - remaining;
}
/**
* Get the count of a specific item type on this disk.
*/
public int getCount(String itemId) {
if (itemId == null) return 0;
int total = 0;
for (StorageSlot slot : slots) {
if (!slot.isEmpty() && itemId.equals(slot.getItemId())) {
total += slot.getCount();
}
}
return total;
}
/**
* Get all items on this disk as a flat map (itemId -> total count).
*/
public Map<String, Long> getAllItems() {
Map<String, Long> result = new HashMap<>();
for (StorageSlot slot : slots) {
if (slot.isEmpty()) continue;
result.merge(slot.getItemId(), (long) slot.getCount(), Long::sum);
}
return result;
}
/**
* Get distinct item types on this disk.
*/
public int getDistinctItemTypes() {
return (int) slots.stream()
.filter(s -> !s.isEmpty())
.map(StorageSlot::getItemId)
.distinct()
.count();
}
/**
* Clear all items from this disk (format operation).
*/
public void clear() {
for (int i = 0; i < maxSlots; i++) {
slots.set(i, new StorageSlot());
}
usedSlots = 0;
itemCount = 0;
}
public boolean isFull() {
return usedSlots >= maxSlots;
}
public boolean isEmpty() {
return itemCount <= 0;
}
/**
* Get an unmodifiable view of all slots.
*/
public List<StorageSlot> getSlots() {
return Collections.unmodifiableList(slots);
}
/**
* Get the used space fraction (0.0 1.0) of this disk.
*/
public double getUsageFraction() {
return (double) usedSlots / maxSlots;
}
private void recalcCounts() {
usedSlots = 0;
itemCount = 0;
for (StorageSlot slot : slots) {
if (!slot.isEmpty()) {
usedSlots++;
itemCount += slot.getCount();
}
}
}
}

View file

@ -0,0 +1,79 @@
package dev.refinedstorage.storage;
/**
* A single slot in a StorageDisk.
* Each slot holds one item type with a count (max 64).
*/
public final class StorageSlot {
private String itemId;
private int count;
public StorageSlot() {
this.itemId = null;
this.count = 0;
}
public StorageSlot(String itemId, int count) {
this.itemId = itemId;
this.count = Math.max(0, Math.min(count, 64));
}
public boolean isEmpty() {
return itemId == null || count <= 0;
}
public String getItemId() {
return itemId;
}
public int getCount() {
return isEmpty() ? 0 : count;
}
/**
* Try to add items into this slot. Returns the amount that was actually added.
* Fills up to 64 total, returns leftover that didn't fit.
*/
public int addItems(String newItemId, int amount) {
if (amount <= 0 || newItemId == null) return 0;
if (isEmpty()) {
int fit = Math.min(amount, 64);
this.itemId = newItemId;
this.count = fit;
return amount - fit;
}
if (!this.itemId.equals(newItemId)) {
return amount; // cannot mix item types in one slot
}
int space = 64 - this.count;
if (space <= 0) return amount;
int fit = Math.min(amount, space);
this.count += fit;
return amount - fit;
}
/**
* Remove up to {@code amount} items from this slot.
* Returns the actual number removed.
*/
public int removeItems(int amount) {
if (amount <= 0 || isEmpty()) return 0;
int removed = Math.min(amount, this.count);
this.count -= removed;
if (this.count <= 0) {
this.itemId = null;
this.count = 0;
}
return removed;
}
@Override
public String toString() {
return isEmpty() ? "[empty]" : "[" + itemId + " x" + count + "]";
}
}

View file

@ -8,7 +8,10 @@ import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder;
import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.logger.HytaleLogger;
import dev.refinedstorage.network.NetworkManager;
import dev.refinedstorage.storage.DiskDriveManager;
import dev.refinedstorage.storage.StorageDisk;
import java.util.ArrayList;
import java.util.List;
@ -19,6 +22,8 @@ import java.util.List;
*/
public class DiskDriveUI extends CustomUIPage {
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
private final String networkId;
public DiskDriveUI(PlayerRef playerRef, String networkId) {
@ -92,21 +97,54 @@ public class DiskDriveUI extends CustomUIPage {
}
private List<DiskEntry> collectDisks(NetworkManager.Network net) {
// TODO: Actual disk collection from disk storage system
// For now return empty list DiskManager not yet implemented
return new ArrayList<>();
List<DiskEntry> entries = new ArrayList<>();
// Find disk drive positions in this network
List<com.hypixel.hytale.math.vector.Vector3i> drivePositions = new ArrayList<>();
for (java.util.Map.Entry<com.hypixel.hytale.math.vector.Vector3i, String> machine : net.machines.entrySet()) {
if ("diskdrive".equals(machine.getValue())) {
drivePositions.add(machine.getKey());
}
}
DiskDriveManager diskMgr = DiskDriveManager.getInstance();
int diskIndex = 0;
for (com.hypixel.hytale.math.vector.Vector3i drivePos : drivePositions) {
List<StorageDisk> disks = diskMgr.getDisks(drivePos);
for (StorageDisk disk : disks) {
entries.add(new DiskEntry(
disk.getDiskName(),
disk.getItemCount(),
disk.getMaxSlots() * 64, // theoretical max items
disk.getDistinctItemTypes(),
disk.getPriority()
));
diskIndex++;
}
}
return entries;
}
private void formatDisk(int diskIndex) {
// TODO: Format disk (clear all items stored on it)
// Need to map diskIndex back to a specific drive position + slot
// For now placeholder will be implemented with full disk management
LOGGER.atInfo().log("Format disk requested for disk #%d", diskIndex);
}
private void ejectDisk(int diskIndex) {
// TODO: Eject disk (remove from network, spawn as item in world)
// Need to map diskIndex back to a specific drive position + slot
// For now placeholder will be implemented with full disk management
LOGGER.atInfo().log("Eject disk requested for disk #%d", diskIndex);
}
private void setDiskPriority(int diskIndex, int priority) {
// TODO: Update disk priority for item insertion ordering
// Need to map diskIndex back to a specific drive position + slot
// For now placeholder will be implemented with full disk management
LOGGER.atInfo().log("Set disk #%d priority to %d", diskIndex, priority);
}
private void addDiskPanel(UICommandBuilder commands, UIEventBuilder events, int index, DiskEntry disk) {

View file

@ -0,0 +1,123 @@
package dev.refinedstorage.handler;
import com.hypixel.hytale.math.vector.Vector3i;
import dev.refinedstorage.network.NetworkManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for NetworkTickHandler network processing logic.
*
* Verifies power management, machine accounting, and
* inventory propagation in the network tick cycle.
*/
class NetworkTickHandlerTest {
private NetworkManager mgr;
@BeforeEach
void setUp() {
NetworkManager.resetForTesting();
mgr = NetworkManager.getInstance();
}
@Test
@DisplayName("Network has inventory and powered=true by default")
void network_HasInventoryAndPoweredByDefault() {
mgr.onControllerPlaced(v(0, 0, 0));
int netId = mgr.getNetworkForController(v(0, 0, 0));
NetworkManager.Network net = mgr.getNetwork(netId);
assertNotNull(net.inventory, "Network should have an inventory");
assertEquals(0, net.inventory.getTotalItemCount(), "New network should have 0 items");
assertTrue(net.powered, "New network should be powered by default");
}
@Test
@DisplayName("Tick handler skips processing when network is not powered")
void tick_SkipsWhenNotPowered() {
mgr.onControllerPlaced(v(0, 0, 0));
int netId = mgr.getNetworkForController(v(0, 0, 0));
NetworkManager.Network net = mgr.getNetwork(netId);
net.powered = false;
// Add an importer machine
mgr.onMachinePlaced(v(1, 0, 0), "importer");
// The tick handler shouldn't crash it just skips unpowered networks.
// We verify by checking no exception is thrown and inventory stays empty.
// (We can't easily trigger processNetworkTick directly as it's private,
// but we verify the network structure is correct after setup.)
assertEquals(1, net.machines.size(), "Machine should be registered");
assertFalse(net.powered, "Network should remain unpowered");
assertEquals(0, net.inventory.getTotalItemCount(), "No items should be in network");
}
@Test
@DisplayName("Network with multiple machine types is correctly structured")
void network_WithMultipleMachineTypes() {
mgr.onControllerPlaced(v(0, 0, 0));
int netId = mgr.getNetworkForController(v(0, 0, 0));
mgr.onMachinePlaced(v(1, 0, 0), "importer");
mgr.onMachinePlaced(v(2, 0, 0), "exporter");
mgr.onMachinePlaced(v(3, 0, 0), "diskdrive");
mgr.onMachinePlaced(v(4, 0, 0), "grid");
NetworkManager.Network net = mgr.getNetwork(netId);
assertEquals(4, net.machines.size(), "All 4 machines should be registered");
// Verify machine types
assertEquals("importer", net.machines.get(v(1, 0, 0)));
assertEquals("exporter", net.machines.get(v(2, 0, 0)));
assertEquals("diskdrive", net.machines.get(v(3, 0, 0)));
assertEquals("grid", net.machines.get(v(4, 0, 0)));
}
@Test
@DisplayName("Network inventory supports add/remove operations")
void networkInventory_BasicOperations() {
mgr.onControllerPlaced(v(0, 0, 0));
int netId = mgr.getNetworkForController(v(0, 0, 0));
NetworkManager.Network net = mgr.getNetwork(netId);
net.inventory.addItems("minecraft:stone", 64);
assertEquals(64, net.inventory.getCount("minecraft:stone"));
assertEquals(1, net.inventory.getDistinctItemTypes());
assertEquals(64, net.inventory.getTotalItemCount());
net.inventory.addItems("minecraft:oak_log", 32);
assertEquals(2, net.inventory.getDistinctItemTypes());
assertEquals(96, net.inventory.getTotalItemCount());
long removed = net.inventory.removeItems("minecraft:stone", 10);
assertEquals(10, removed);
assertEquals(54, net.inventory.getCount("minecraft:stone"));
assertEquals(86, net.inventory.getTotalItemCount());
net.inventory.clear();
assertEquals(0, net.inventory.getTotalItemCount());
assertEquals(0, net.inventory.getDistinctItemTypes());
}
@Test
@DisplayName("Network powered flag can be toggled")
void network_PoweredFlag_Toggle() {
mgr.onControllerPlaced(v(0, 0, 0));
int netId = mgr.getNetworkForController(v(0, 0, 0));
NetworkManager.Network net = mgr.getNetwork(netId);
assertTrue(net.powered);
net.powered = false;
assertFalse(net.powered);
net.powered = true;
assertTrue(net.powered);
}
private static Vector3i v(int x, int y, int z) {
return new Vector3i(x, y, z);
}
}

View file

@ -0,0 +1,411 @@
package dev.refinedstorage.network;
import com.hypixel.hytale.math.vector.Vector3i;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import static org.junit.jupiter.api.Assertions.*;
/**
* Automated tests for NetworkManager BFS cable propagation.
*
* Tests BFS-based network rebuilding after cable placement/removal,
* cable chaining, branch propagation, and multi-network isolation.
*/
class NetworkManagerTest {
static {
// HytaleLogManager is not available on the test classpath (Server JAR dependency).
// We use the default JUL log manager instead; tests don't rely on Hytale logging.
}
private NetworkManager mgr;
@BeforeEach
void setUp() {
NetworkManager.resetForTesting();
mgr = NetworkManager.getInstance();
}
// ========================================================================
// Network Creation
// ========================================================================
@Test
@DisplayName("Placing a controller creates a new network with that position")
void controllerPlaced_CreatesNetwork() {
Vector3i pos = v(0, 0, 0);
mgr.onControllerPlaced(pos);
int netId = mgr.getNetworkForController(pos);
assertTrue(netId > 0, "Controller should be registered in a network");
NetworkManager.Network net = mgr.getNetwork(netId);
assertNotNull(net, "Network should exist");
assertEquals(pos, net.controllerPos, "Network controller position should match");
}
@Test
@DisplayName("Multiple controllers create separate networks with unique IDs")
void multipleControllers_CreateSeparateNetworks() {
mgr.onControllerPlaced(v(0, 0, 0));
mgr.onControllerPlaced(v(10, 0, 10));
int net1 = mgr.getNetworkForController(v(0, 0, 0));
int net2 = mgr.getNetworkForController(v(10, 0, 10));
assertNotEquals(net1, net2, "Each controller should have a unique network ID");
}
@Test
@DisplayName("GetAllNetworkIds returns all networks")
void getAllNetworkIds_ReturnsAll() {
mgr.onControllerPlaced(v(0, 0, 0));
mgr.onControllerPlaced(v(5, 0, 0));
mgr.onControllerPlaced(v(10, 0, 0));
assertEquals(3, mgr.getAllNetworkIds().size(), "Should report 3 networks");
}
// ========================================================================
// Cable Propagation BFS Chaining
// ========================================================================
@Test
@DisplayName("Connecting a cable adjacent to a controller adds it to the network")
void cablePlaced_AdjacentToController_AddedToNetwork() {
mgr.onControllerPlaced(v(0, 0, 0));
mgr.onCablePlaced(v(1, 0, 0)); // right of controller
int netId = mgr.getNetworkForCable(v(1, 0, 0));
assertTrue(netId > 0, "Cable adjacent to controller should be in a network");
}
@Test
@DisplayName("Cable chain propagates BFS through multiple cables")
void cableChain_BFSPropagatesToAllCables() {
mgr.onControllerPlaced(v(0, 0, 0));
// Place a straight line of 3 cables: controller cable1 cable2 cable3
mgr.onCablePlaced(v(1, 0, 0)); // cable1
mgr.onCablePlaced(v(2, 0, 0)); // cable2
mgr.onCablePlaced(v(3, 0, 0)); // cable3
int netId = mgr.getNetworkForCable(v(3, 0, 0));
assertTrue(netId > 0, "End of cable chain should be in network");
assertEquals(mgr.getNetworkForCable(v(1, 0, 0)), netId, "All cables should share same network ID");
// Verify all cables are registered
NetworkManager.Network net = mgr.getNetwork(netId);
assertTrue(net.cables.size() >= 3, "Network should contain at least 3 cables");
}
@Test
@DisplayName("BFS propagates through branches (Y-shaped cable layout)")
void branchPropagation_YShaped_AllCablesConnected() {
mgr.onControllerPlaced(v(0, 0, 0));
// Trunk: controller (1,0,0) (2,0,0)
mgr.onCablePlaced(v(1, 0, 0));
mgr.onCablePlaced(v(2, 0, 0));
// Branch 1: (2,0,0) (2,1,0) (2,2,0)
mgr.onCablePlaced(v(2, 1, 0));
mgr.onCablePlaced(v(2, 2, 0));
// Branch 2: (2,0,0) (2,0,1) (2,0,2)
mgr.onCablePlaced(v(2, 0, 1));
mgr.onCablePlaced(v(2, 0, 2));
int netId = mgr.getNetworkForCable(v(1, 0, 0));
assertTrue(netId > 0, "Trunk cable should have network");
assertEquals(netId, mgr.getNetworkForCable(v(2, 2, 0)), "Branch 1 end should be same network");
assertEquals(netId, mgr.getNetworkForCable(v(2, 0, 2)), "Branch 2 end should be same network");
NetworkManager.Network net = mgr.getNetwork(netId);
assertEquals(6, net.cables.size(), "All 6 cables should be in the network");
}
@Test
@DisplayName("Placing a cable not adjacent to any network returns -1 and is not added")
void cablePlaced_NotAdjacent_NotAdded() {
mgr.onControllerPlaced(v(0, 0, 0));
// This is too far (gap of 2 blocks)
mgr.onCablePlaced(v(3, 0, 0));
assertEquals(-1, mgr.getNetworkForCable(v(3, 0, 0)),
"Isolated cable should not be in any network");
}
// ========================================================================
// Cable Removal Network Split
// ========================================================================
@Test
@DisplayName("Breaking a cable in the middle splits the network")
void cableRemoved_MiddleCable_SplitsNetwork() {
mgr.onControllerPlaced(v(0, 0, 0));
mgr.onCablePlaced(v(1, 0, 0));
mgr.onCablePlaced(v(2, 0, 0));
mgr.onCablePlaced(v(3, 0, 0));
int originalNetId = mgr.getNetworkForCable(v(1, 0, 0));
// Remove middle cable - breaks the chain
mgr.onBlockBroken(v(2, 0, 0));
// Cable1 should still be on the network (connected to controller)
assertEquals(originalNetId, mgr.getNetworkForCable(v(1, 0, 0)),
"Cable still connected to controller should keep its network");
// Cable3 should be orphaned (no longer reachable from controller)
assertEquals(-1, mgr.getNetworkForCable(v(3, 0, 0)),
"Cable beyond break should no longer be in the network");
}
@Test
@DisplayName("Removing all cables leaves only the controller in the network")
void removeAllCables_OnlyControllerRemains() {
mgr.onControllerPlaced(v(0, 0, 0));
mgr.onCablePlaced(v(1, 0, 0));
mgr.onCablePlaced(v(2, 0, 0));
int netId = mgr.getNetworkForController(v(0, 0, 0));
mgr.onBlockBroken(v(1, 0, 0));
mgr.onBlockBroken(v(2, 0, 0));
// Controller should still exist in its network
assertEquals(netId, mgr.getNetworkForController(v(0, 0, 0)),
"Controller should still have its network");
// No cables should be in the network
NetworkManager.Network net = mgr.getNetwork(netId);
assertNotNull(net, "Network should still exist");
assertTrue(net.cables.isEmpty(), "Network should have no cables after all removed");
}
@Test
@DisplayName("Breaking the controller removes the entire network")
void breakController_RemovesEntireNetwork() {
mgr.onControllerPlaced(v(0, 0, 0));
mgr.onCablePlaced(v(1, 0, 0));
mgr.onCablePlaced(v(2, 0, 0));
mgr.onBlockBroken(v(0, 0, 0));
assertEquals(-1, mgr.getNetworkForController(v(0, 0, 0)),
"Broken controller should not be in any network");
assertEquals(-1, mgr.getNetworkForCable(v(1, 0, 0)),
"Cables from removed network should be orphaned");
assertEquals(-1, mgr.getNetworkForCable(v(2, 0, 0)),
"All cables from removed network should be orphaned");
assertTrue(mgr.getAllNetworkIds().isEmpty(),
"No networks should remain after controller removal");
}
// ========================================================================
// Machine Placement
// ========================================================================
@Test
@DisplayName("Machine placed adjacent to cable becomes part of the network")
void machinePlaced_AdjacentToCable_JoinsNetwork() {
mgr.onControllerPlaced(v(0, 0, 0));
mgr.onCablePlaced(v(1, 0, 0));
int netId = mgr.getNetworkForCable(v(1, 0, 0));
mgr.onMachinePlaced(v(1, 1, 0), "disk_drive");
assertEquals(netId, mgr.getNetworkForMachine(v(1, 1, 0)),
"Machine adjacent to cable should join the network");
}
@Test
@DisplayName("Machine not adjacent to network stays isolated")
void machinePlaced_NotAdjacent_StaysIsolated() {
mgr.onControllerPlaced(v(0, 0, 0));
mgr.onMachinePlaced(v(5, 0, 0), "disk_drive");
assertEquals(-1, mgr.getNetworkForMachine(v(5, 0, 0)),
"Isolated machine should not be added to any network");
}
@Test
@DisplayName("Multiple machines can be on the same network")
void multipleMachines_SameNetwork() {
mgr.onControllerPlaced(v(0, 0, 0));
mgr.onCablePlaced(v(1, 0, 0));
mgr.onCablePlaced(v(2, 0, 0));
int netId = mgr.getNetworkForCable(v(1, 0, 0));
mgr.onMachinePlaced(v(1, 1, 0), "disk_drive");
mgr.onMachinePlaced(v(2, 1, 0), "crafter");
assertEquals(netId, mgr.getNetworkForMachine(v(1, 1, 0)));
assertEquals(netId, mgr.getNetworkForMachine(v(2, 1, 0)));
}
@Test
@DisplayName("Breaking a controller removes all associated machines from lookup")
void breakController_RemovesMachines() {
mgr.onControllerPlaced(v(0, 0, 0));
mgr.onCablePlaced(v(1, 0, 0));
mgr.onMachinePlaced(v(1, 1, 0), "disk_drive");
mgr.onBlockBroken(v(0, 0, 0));
assertEquals(-1, mgr.getNetworkForMachine(v(1, 1, 0)),
"Machine should be orphaned after controller break");
}
// ========================================================================
// Mixed Layout Realistic Scenarios
// ========================================================================
@Test
@DisplayName("Full layout: controller with multiple cables and machines — BFS covers all")
void fullLayout_BFSCoversAllComponents() {
// Layout (2D top-down):
// C = controller at (0,0,0)
// - = cable
// D = disk drive
// F = furnace / crafter
//
// C - - D
// |
// F
//
mgr.onControllerPlaced(v(0, 0, 0));
mgr.onCablePlaced(v(1, 0, 0));
mgr.onCablePlaced(v(2, 0, 0));
mgr.onCablePlaced(v(2, 1, 0)); // vertical branch down
mgr.onMachinePlaced(v(3, 0, 0), "disk_drive");
mgr.onMachinePlaced(v(2, 2, 0), "crafter");
int netId = mgr.getNetworkForController(v(0, 0, 0));
NetworkManager.Network net = mgr.getNetwork(netId);
assertEquals(3, net.cables.size(), "All 3 cables should be in the network");
assertEquals(2, net.machines.size(), "Both machines should be in the network");
}
@Test
@DisplayName("Machine connected directly adjacent to controller is registered")
void machinePlaced_AdjacentToController_Registered() {
mgr.onControllerPlaced(v(0, 0, 0));
mgr.onMachinePlaced(v(0, 1, 0), "disk_drive");
int netId = mgr.getNetworkForMachine(v(0, 1, 0));
assertTrue(netId > 0, "Machine adjacent to controller should join network");
}
@Test
@DisplayName("findAdjacentNetwork checks cables, controllers, and machines")
void findAdjacentNetwork_ScansAllNeighbors() {
// Place machine first, then cable adjacent to it, then controller adjacent to cable
mgr.onControllerPlaced(v(5, 0, 0));
mgr.onCablePlaced(v(4, 0, 0));
// Now place a cable adjacent to cable (4,0,0) but on the far side
mgr.onCablePlaced(v(3, 0, 0));
int netId = mgr.getNetworkForCable(v(3, 0, 0));
assertTrue(netId > 0, "Cable should find network through neighbor cable");
}
// ========================================================================
// Edge Cases
// ========================================================================
@Test
@DisplayName("Breaking a non-existent block does nothing")
void breakNonExistentBlock_NoError() {
// Should not throw
mgr.onBlockBroken(v(99, 99, 99));
// Also should not throw on empty manager
assertTrue(mgr.getAllNetworkIds().isEmpty());
}
@Test
@DisplayName("Placing duplicate cables doesn't break anything")
void duplicateCablePlacement_Idempotent() {
mgr.onControllerPlaced(v(0, 0, 0));
mgr.onCablePlaced(v(1, 0, 0));
mgr.onCablePlaced(v(1, 0, 0)); // place same cable again
int netId = mgr.getNetworkForCable(v(1, 0, 0));
assertTrue(netId > 0, "Duplicate cable placement should still work");
// This test verifies no crash; the Network uses a List so duplicates are allowed there.
// We just verify the cable is still in the lookup.
}
@Test
@DisplayName("3D propagation: cables can go up and down")
void threeDimensionalPropagation_CablesAboveAndBelow() {
mgr.onControllerPlaced(v(0, 0, 0));
mgr.onCablePlaced(v(0, 1, 0)); // up
mgr.onCablePlaced(v(0, -1, 0)); // down
mgr.onCablePlaced(v(0, 0, 1)); // north
mgr.onCablePlaced(v(0, 0, -1)); // south
int netId = mgr.getNetworkForController(v(0, 0, 0));
assertTrue(netId > 0, "Controller should belong to a network");
netId = mgr.getNetworkForCable(v(0, 1, 0));
assertTrue(netId > 0, "Cable above should be connected");
assertEquals(netId, mgr.getNetworkForCable(v(0, -1, 0)),
"Cable below should be same network");
assertEquals(netId, mgr.getNetworkForCable(v(0, 0, 1)),
"Cable north should be same network");
assertEquals(netId, mgr.getNetworkForCable(v(0, 0, -1)),
"Cable south should be same network");
}
@Test
@DisplayName("Rebuild network after breaking a cable only keeps reachable components")
void rebuildAfterBreak_OnlyReachableComponentsRemain() {
// Layout: C(0,0,0) - (1,0,0) - (2,0,0) - (3,0,0)
// |
// (2,1,0) D
mgr.onControllerPlaced(v(0, 0, 0));
mgr.onCablePlaced(v(1, 0, 0));
mgr.onCablePlaced(v(2, 0, 0));
mgr.onCablePlaced(v(3, 0, 0));
mgr.onCablePlaced(v(2, 1, 0));
mgr.onMachinePlaced(v(2, 2, 0), "disk_drive");
int netId = mgr.getNetworkForController(v(0, 0, 0));
// Break the cable at (1,0,0) this splits (0,0,0) from everything else... no wait,
// (1,0,0) IS directly adjacent to controller. So breaking it disconnects the rest.
// Actually the rebuild happens inside removeCable which calls rebuildNetwork on that network.
// After rebuild: only controller remains reachable, (1,0,0) was removed.
// But (1,0,0) is the one being removed, so after rebuild:
// From controller (0,0,0), neighbors: only removed cable (1,0,0) nothing left.
mgr.onBlockBroken(v(1, 0, 0));
// Controller should still be in its network
assertEquals(netId, mgr.getNetworkForController(v(0, 0, 0)),
"Controller should still have its network");
// Cables beyond the break should be orphaned
assertEquals(-1, mgr.getNetworkForCable(v(2, 0, 0)),
"Cable beyond break should be orphaned");
assertEquals(-1, mgr.getNetworkForCable(v(3, 0, 0)),
"Far cable should be orphaned");
assertEquals(-1, mgr.getNetworkForCable(v(2, 1, 0)),
"Branch cable should be orphaned");
// Machine beyond break should be orphaned
assertEquals(-1, mgr.getNetworkForMachine(v(2, 2, 0)),
"Machine beyond break should be orphaned");
}
// ========================================================================
// Helpers
// ========================================================================
private static Vector3i v(int x, int y, int z) {
return new Vector3i(x, y, z);
}
}

8
start-server.sh Executable file
View file

@ -0,0 +1,8 @@
#!/bin/bash
export JAVA_HOME=/home/nepharius/tools/jdk-25.0.3+9
export _JAVA_OPTIONS="-Xmx16G -Xms4G"
cd /home/nepharius/projects/hytale-refined-storage
echo "Starting Hytale Server..."
_JAVA_OPTIONS="$_JAVA_OPTIONS" ./gradlew runServer 2>&1
echo "Server process exited with code $?"
exit $?