diff --git a/src/main/java/turniplabs/halplibe/helper/BlockHelper.java b/src/main/java/turniplabs/halplibe/helper/BlockHelper.java index 3913f0f..613a830 100644 --- a/src/main/java/turniplabs/halplibe/helper/BlockHelper.java +++ b/src/main/java/turniplabs/halplibe/helper/BlockHelper.java @@ -6,14 +6,22 @@ import net.minecraft.core.item.Item; import net.minecraft.core.item.block.ItemBlock; import turniplabs.halplibe.mixin.accessors.BlockAccessor; +import turniplabs.halplibe.util.registry.IdSupplier; +import turniplabs.halplibe.util.registry.RunLengthConfig; +import turniplabs.halplibe.util.registry.RunReserves; +import turniplabs.halplibe.util.toml.Toml; -import java.util.ArrayList; +import java.util.function.Consumer; -@Deprecated public class BlockHelper { public static int highestVanilla; - + + private static final RunReserves reserves = new RunReserves( + BlockHelper::findOpenIds, + BlockHelper::findLength + ); + /** * Should be called in a runnable scheduled with {@link RegistryHelper#scheduleRegistry(boolean, Runnable)} * @param count the amount of needed blocks for the mod @@ -22,7 +30,7 @@ public class BlockHelper { public static int findOpenIds(int count) { int run = 0; for (int i = highestVanilla; i < Block.blocksList.length; i++) { - if (Block.blocksList[i] == null) { + if (Block.blocksList[i] == null && !reserves.isReserved(i)) { if (run >= count) return (i - run); run++; @@ -32,6 +40,39 @@ public static int findOpenIds(int count) { } return -1; } + + public static int findLength(int id, int terminate) { + int run = 0; + for (int i = id; i < Block.blocksList.length; i++) { + if (Block.blocksList[i] == null && !reserves.isReserved(i)) { + run++; + if (run >= terminate) return terminate; + } else { + return run; + } + } + return run; + } + + /** + * Allows halplibe to automatically figure out where to insert the runs + * @param modid an identifier for the mod, can be anything, but should be something the user can identify + * @param runs a toml object representing configured registry runs + * @param neededIds the number of needed ids + * if this changes after the mod has been configured (i.e. mod updated and now has more blocks) it'll find new, valid runs to put those blocks into + * @param function the function to run for registering items + */ + public static void reserveRuns(String modid, Toml runs, int neededIds, Consumer function) { + RunLengthConfig cfg = new RunLengthConfig(runs, neededIds); + cfg.register(reserves); + RegistryHelper.scheduleSmartRegistry( + () -> { + IdSupplier supplier = new IdSupplier(modid, reserves, cfg, neededIds); + function.accept(supplier); + supplier.validate(); + } + ); + } @Deprecated public static Block createBlock(String modId, Block block, String texture, BlockSound stepSound, float hardness, float resistance, float lightValue) { diff --git a/src/main/java/turniplabs/halplibe/helper/ItemHelper.java b/src/main/java/turniplabs/halplibe/helper/ItemHelper.java index cc0c607..83b2f06 100644 --- a/src/main/java/turniplabs/halplibe/helper/ItemHelper.java +++ b/src/main/java/turniplabs/halplibe/helper/ItemHelper.java @@ -4,11 +4,22 @@ import net.minecraft.core.item.Item; import turniplabs.halplibe.HalpLibe; import turniplabs.halplibe.mixin.mixins.registry.BlockMixin; +import turniplabs.halplibe.util.registry.IdSupplier; +import turniplabs.halplibe.util.registry.RunLengthConfig; +import turniplabs.halplibe.util.registry.RunReserves; +import turniplabs.halplibe.util.toml.Toml; + +import java.util.function.Consumer; public class ItemHelper { public static int highestVanilla; - + + private static final RunReserves reserves = new RunReserves( + ItemHelper::findOpenIds, + ItemHelper::findLength + ); + /** * Should be called in a runnable scheduled with {@link RegistryHelper#scheduleRegistry(boolean, Runnable)} * @param count the amount of needed blocks for the mod @@ -20,7 +31,7 @@ public static int findOpenIds(int count) { // block ids should always match the id of their corresponding item // therefor, start registering items one after the max block id for (int i = Block.blocksList.length + 1; i < Item.itemsList.length; i++) { - if (Item.itemsList[i] == null) { + if (Item.itemsList[i] == null && !reserves.isReserved(i)) { if (run >= count) return (i - run); run++; @@ -30,7 +41,40 @@ public static int findOpenIds(int count) { } return -1; } - + + public static int findLength(int id, int terminate) { + int run = 0; + for (int i = id; i < Item.itemsList.length; i++) { + if (Item.itemsList[i] == null && !reserves.isReserved(i)) { + run++; + if (run >= terminate) return terminate; + } else { + return run; + } + } + return run; + } + + /** + * Allows halplibe to automatically figure out where to insert the runs + * @param modid an identifier for the mod, can be anything, but should be something the user can identify + * @param runs a toml object representing configured registry runs + * @param neededIds the number of needed ids + * if this changes after the mod has been configured (i.e. mod updated and now has more items) it'll find new, valid runs to put those items into + * @param function the function to run for registering items + */ + public static void reserveRuns(String modid, Toml runs, int neededIds, Consumer function) { + RunLengthConfig cfg = new RunLengthConfig(runs, neededIds); + cfg.register(reserves); + RegistryHelper.scheduleSmartRegistry( + () -> { + IdSupplier supplier = new IdSupplier(modid, reserves, cfg, neededIds); + function.accept(supplier); + supplier.validate(); + } + ); + } + public static Item createItem(String modId, Item item, String translationKey, String texture) { int[] mainCoords = TextureHelper.getOrCreateItemTexture(modId, texture); item.setIconCoord(mainCoords[0], mainCoords[1]); diff --git a/src/main/java/turniplabs/halplibe/helper/RegistryHelper.java b/src/main/java/turniplabs/halplibe/helper/RegistryHelper.java index 517986b..dfec147 100644 --- a/src/main/java/turniplabs/halplibe/helper/RegistryHelper.java +++ b/src/main/java/turniplabs/halplibe/helper/RegistryHelper.java @@ -1,19 +1,45 @@ package turniplabs.halplibe.helper; +import turniplabs.halplibe.util.toml.Toml; + import java.util.ArrayList; +import java.util.function.Consumer; public class RegistryHelper { private static final ArrayList regsitryFunctions = new ArrayList<>(); - + private static final ArrayList configuredRegsitryFunctions = new ArrayList<>(); + private static final ArrayList smartRregsitryFunctions = new ArrayList<>(); + + /** + * Only intended for internal use from {@link BlockHelper#reserveRuns(Toml, int, Consumer)} and {@link ItemHelper#reserveRuns(Toml, int, Consumer)} + * + * + * @param function the function to run on registry handling + */ + public static void scheduleSmartRegistry(Runnable function) { + smartRregsitryFunctions.add(function); + } + + /** + * For blocks and items, use {@link BlockHelper#reserveRuns(Toml, int, Consumer)} and {@link ItemHelper#reserveRuns(Toml, int, Consumer)}, respectively + * These will figure out what ids are available automatically, making sure to account for mods that aren't using halplibe, or are using {@link RegistryHelper#scheduleRegistry(boolean, Runnable)} + * + * Reason this is not deprecated: + * - other registries that halplibe doesn't already have utils for + * - mods that already have their item count fully defined from the start (i.e. some mod author already knows they will only ever have 2 items) + * + * @param configured if the mod has already been configured in the past + * @param function the function to run upon registering stuff + */ public static void scheduleRegistry(boolean configured, Runnable function) { - if (configured) regsitryFunctions.add(0, function); + if (configured) configuredRegsitryFunctions.add(function); else regsitryFunctions.add(function); } @SuppressWarnings("unused") private static void runRegistry() { - for (Runnable regsitryFunction : regsitryFunctions) { - regsitryFunction.run(); - } + for (Runnable regsitryFunction : configuredRegsitryFunctions) regsitryFunction.run(); + for (Runnable regsitryFunction : smartRregsitryFunctions) regsitryFunction.run(); + for (Runnable regsitryFunction : regsitryFunctions) regsitryFunction.run(); } } diff --git a/src/main/java/turniplabs/halplibe/mixin/mixins/registry/BlockMixin.java b/src/main/java/turniplabs/halplibe/mixin/mixins/registry/BlockMixin.java index 6ea3aaa..242edf1 100644 --- a/src/main/java/turniplabs/halplibe/mixin/mixins/registry/BlockMixin.java +++ b/src/main/java/turniplabs/halplibe/mixin/mixins/registry/BlockMixin.java @@ -1,6 +1,7 @@ package turniplabs.halplibe.mixin.mixins.registry; import net.minecraft.core.block.Block; +import net.minecraft.core.block.material.Material; import org.spongepowered.asm.mixin.*; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -9,15 +10,25 @@ import turniplabs.halplibe.helper.BlockHelper; @Mixin(value = Block.class, remap = false) -public class BlockMixin { +public abstract class BlockMixin { @Shadow public static int highestBlockId; @Shadow @Final public int id; - + + @Shadow @Final public static Block[] blocksList; + + @Shadow public abstract String getKey(); + @Inject(at = @At("TAIL"), method = "") private static void captureHighest(CallbackInfo ci) { BlockHelper.highestVanilla = highestBlockId; } + + @Inject(at = @At(value = "INVOKE", target = "Ljava/lang/IllegalArgumentException;(Ljava/lang/String;)V", shift = At.Shift.BEFORE), method = "") + public void addInfo(String key, int id, Material material, CallbackInfo ci) { + throw new IllegalArgumentException("Slot " + id + " is already occupied by " + blocksList[id].getKey() + " when adding " + key); + } + @Redirect(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/achievement/stat/StatList;onBlockInit()V")) private static void delayInit() { } diff --git a/src/main/java/turniplabs/halplibe/util/TomlConfigHandler.java b/src/main/java/turniplabs/halplibe/util/TomlConfigHandler.java index 6731262..7b89f9b 100644 --- a/src/main/java/turniplabs/halplibe/util/TomlConfigHandler.java +++ b/src/main/java/turniplabs/halplibe/util/TomlConfigHandler.java @@ -11,6 +11,7 @@ public class TomlConfigHandler { private static final String CONFIG_DIRECTORY = FabricLoader.getInstance().getGameDir().toString() + "/config/"; private final Toml defaults; private final Toml config; + private Toml rawParsed; private String configFileName = ""; private ConfigUpdater updater; @@ -86,10 +87,15 @@ public void writeConfig() { // make sure the actual config has all the required entries config.merge(defaults); + if (rawParsed != null) { + // preserve undefined entries + // used due to run config handler + rawParsed.merge(true, config); + } else rawParsed = config; // write the config try (OutputStream output = new FileOutputStream(configFile)) { - output.write(config.toString().getBytes()); + output.write(rawParsed.toString().getBytes()); output.close(); } catch (IOException e) { e.printStackTrace(); @@ -115,13 +121,27 @@ private void loadConfig(File configFile, Toml properties) { } Toml parsed = TomlParser.parse(baos.toString()); - updater.updating = parsed; - updater.update(); - properties.merge(true, parsed); + + // TODO: system for specifying "greedy" categories? + // greedy categories would keep all entries that aren't sepcified in code but are specified in the config + if (defaults.getComment().isPresent()) { + rawParsed = new Toml(defaults.getComment().get()); + rawParsed.addMissing(parsed); + } else rawParsed = parsed; + + if (updater != null) { + updater.updating = rawParsed; + updater.update(); + } + properties.merge(true, rawParsed); input.close(); } catch (IOException e) { e.printStackTrace(); } } + + public Toml getRawParsed() { + return rawParsed; + } } \ No newline at end of file diff --git a/src/main/java/turniplabs/halplibe/util/registry/IdSupplier.java b/src/main/java/turniplabs/halplibe/util/registry/IdSupplier.java new file mode 100644 index 0000000..435b86e --- /dev/null +++ b/src/main/java/turniplabs/halplibe/util/registry/IdSupplier.java @@ -0,0 +1,164 @@ +package turniplabs.halplibe.util.registry; + +import turniplabs.halplibe.util.registry.error.RequestCutShortException; +import turniplabs.halplibe.util.registry.error.RequestOutOfBoundsException; + +import java.util.ArrayList; +import java.util.List; + +public class IdSupplier { + String modid; + RunReserves reserves; + RunLengthConfig cfg; + int max; + + /* the list of reservations used by the code requesting the ids */ + ArrayList reservations = new ArrayList<>(); + /* if this is true, the reservations will get optimized when being written */ + boolean hasUnreserved = false; + + public IdSupplier(String modid, RunReserves reserves, RunLengthConfig cfg, int max) { + this.modid = modid; + this.reserves = reserves; + this.cfg = cfg; + this.max = max; + + if (cfg.reservations[0].reserved) { + reservations.add(cfg.reservations[0]); + + reservationStart = cfg.reservations[0].start; + reservationEnd = cfg.reservations[0].end; + } + + for (Reservation reservation : cfg.reservations) { + if (!reservation.reserved) { + hasUnreserved = true; + break; + } + } + } + + /* information about the reservation currently being filled */ + int reservationId = 0; + int current = 0; + int done = 0; + + int reservationStart = -1; + int reservationEnd = -1; + + /** + * Finds the next available id + * If you remove a block/item, it should currently just be replaced with a call to this to prevent id shifting + * + * @return the id + */ + public int next() { + Reservation workingIn = cfg.reservations[reservationId]; + if ( + workingIn.reserved && + workingIn.start + current > workingIn.end + ) { + reservationId++; + current = 0; + workingIn = cfg.reservations[reservationId]; + if (workingIn.reserved) + reservations.add(workingIn); + + reservationStart = workingIn.start; + reservationEnd = workingIn.end; + } + if (!workingIn.reserved) { + if ( + reservationStart == -1 || + reservationStart + current >= reservationEnd + ) { + if (reservationStart != -1) { + reservations.add(new Reservation(reservationStart, reservationStart + current - 1)); + } + + reservationStart = reserves.idFinder.apply(0); + reservationEnd = reservationStart + reserves.runLengthFinder.apply(reservationStart, max - done); + current = 0; + } + } + + if (done > max) { + throw new RequestOutOfBoundsException(modid + " has grabbed more ids than it has requested."); + } + + done++; + if (done == max) { + int id = reservationStart + current++; + cfg.write(calculateRuns()); + return id; + } + return reservationStart + current++; + } + + public List calculateRuns() { + if (!cfg.reservations[reservationId].reserved) { + if (reservationStart != -1 && current != 0) { + reservations.add(new Reservation(reservationStart, reservationStart + current - 1)); + reservationId = -1; + reservationStart = -1; + reservationEnd = -1; + current = 0; + } + } + + if (hasUnreserved) { + // group newly added reservations with existing ones if possible + List result = new ArrayList<>(); + Reservation prev = null; + for (Reservation reservation : reservations) { + if (prev == null) { + prev = reservation; + result.add(reservation); + } else { + if (prev.end + 1 == reservation.start) { + result.remove(result.size() - 1); + result.add(new Reservation(prev.start, reservation.end)); + } else { + result.add(reservation); + } + prev = reservation; + } + } + + return result; + } + + return reservations; + } + + /** + * Ensures that in the current registry run has at least {@param amount} ids remaining + * Used for blocks/items that look for hardcoded ids, such as furnaces which look for id+1 for active and id-1 for inactive + * + * @param amount the amount of ids to ensure + */ + public void ensureFree(int amount) { + Reservation workingIn = cfg.reservations[reservationId]; + if (workingIn.reserved) return; + + int remaining = reservationEnd - (reservationStart + current); + System.out.println(remaining); + if (remaining >= amount) { + return; + } + + if (reservationStart != -1) { + reservations.add(new Reservation(reservationStart, reservationStart + current - 1)); + } + + reservationStart = reserves.idFinder.apply(amount); + reservationEnd = reservationStart + reserves.runLengthFinder.apply(reservationStart, max - done); + current = 0; + } + + public void validate() { + if (done != max) { + throw new RequestCutShortException(modid + " did not use up all requested ids."); + } + } +} diff --git a/src/main/java/turniplabs/halplibe/util/registry/Reservation.java b/src/main/java/turniplabs/halplibe/util/registry/Reservation.java new file mode 100644 index 0000000..f6f1ab9 --- /dev/null +++ b/src/main/java/turniplabs/halplibe/util/registry/Reservation.java @@ -0,0 +1,18 @@ +package turniplabs.halplibe.util.registry; + +public class Reservation { + int start, end; + boolean reserved; + + public Reservation(int start, int end) { + this.start = start; + this.end = end; + reserved = true; + } + + public Reservation() { + start = -1; + end = -1; + reserved = false; + } +} diff --git a/src/main/java/turniplabs/halplibe/util/registry/RunLengthConfig.java b/src/main/java/turniplabs/halplibe/util/registry/RunLengthConfig.java new file mode 100644 index 0000000..b420200 --- /dev/null +++ b/src/main/java/turniplabs/halplibe/util/registry/RunLengthConfig.java @@ -0,0 +1,67 @@ +package turniplabs.halplibe.util.registry; + +import turniplabs.halplibe.util.toml.Toml; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class RunLengthConfig { + public Reservation[] reservations; + + Toml config; + + public RunLengthConfig(Toml toml, int requested) { + this.config = toml; + + ArrayList sortedKeys = new ArrayList<>(); + for (String orderedKey : toml.getOrderedKeys()) { + sortedKeys.add( + Integer.parseInt(orderedKey.substring(3)) + ); + } + sortedKeys.sort(Integer::compare); + + int reserved = 0; + ArrayList reservations1 = new ArrayList<>(); + for (Integer sortedKey : sortedKeys) { + String run = toml.get("run" + sortedKey, String.class); + if (run.contains(",")) { + String[] split = run.split(","); + int start = Integer.parseInt(split[0].trim()); + int end = Integer.parseInt(split[1].trim()); + + reserved += end - start + 1; + reservations1.add(new Reservation(start, end)); + } else { + int index = Integer.parseInt(run); + reservations1.add(new Reservation(index, index)); + } + } + if (reserved < requested) { + reservations1.add(new Reservation()); + } + this.reservations = reservations1.toArray(new Reservation[0]); + } + + public void write(List collectedReservations) { + for (String orderedKey : config.getOrderedKeys()) + config.remove(orderedKey); + + int reserve = 0; + for (Reservation collectedReservation : collectedReservations) { + if (collectedReservation.start == collectedReservation.end) + config.addEntry("run" + reserve, "" + collectedReservation.start); + else + config.addEntry("run" + reserve, collectedReservation.start + ", " + collectedReservation.end); + reserve++; + } + } + + public void register(RunReserves reserves) { + for (Reservation reservation : this.reservations) { + if (reservation.reserved) + reserves.reservations.add(reservation); + } + } +} diff --git a/src/main/java/turniplabs/halplibe/util/registry/RunReserves.java b/src/main/java/turniplabs/halplibe/util/registry/RunReserves.java new file mode 100644 index 0000000..fe4c16d --- /dev/null +++ b/src/main/java/turniplabs/halplibe/util/registry/RunReserves.java @@ -0,0 +1,32 @@ +package turniplabs.halplibe.util.registry; + +import java.util.ArrayList; +import java.util.function.BiFunction; +import java.util.function.Function; + +public class RunReserves { + ArrayList reservations = new ArrayList<>(); + + // arg: min + // result: a run with at least the specified length + Function idFinder; + // args: id, min + // result: the length of the run, or min if it's longer than min + BiFunction runLengthFinder; + + public RunReserves(Function idFinder, BiFunction runLengthFinder) { + this.idFinder = idFinder; + this.runLengthFinder = runLengthFinder; + } + + public boolean isReserved(int id) { + for (Reservation reservation : reservations) { + if (!reservation.reserved) continue; + + if (reservation.start <= id && reservation.end >= id) + return true; + } + + return false; + } +} diff --git a/src/main/java/turniplabs/halplibe/util/registry/error/RequestCutShortException.java b/src/main/java/turniplabs/halplibe/util/registry/error/RequestCutShortException.java new file mode 100644 index 0000000..7836556 --- /dev/null +++ b/src/main/java/turniplabs/halplibe/util/registry/error/RequestCutShortException.java @@ -0,0 +1,7 @@ +package turniplabs.halplibe.util.registry.error; + +public class RequestCutShortException extends RuntimeException { + public RequestCutShortException(String message) { + super(message); + } +} diff --git a/src/main/java/turniplabs/halplibe/util/registry/error/RequestOutOfBoundsException.java b/src/main/java/turniplabs/halplibe/util/registry/error/RequestOutOfBoundsException.java new file mode 100644 index 0000000..4d71098 --- /dev/null +++ b/src/main/java/turniplabs/halplibe/util/registry/error/RequestOutOfBoundsException.java @@ -0,0 +1,7 @@ +package turniplabs.halplibe.util.registry.error; + +public class RequestOutOfBoundsException extends RuntimeException { + public RequestOutOfBoundsException(String message) { + super(message); + } +} diff --git a/src/main/java/turniplabs/halplibe/util/toml/Toml.java b/src/main/java/turniplabs/halplibe/util/toml/Toml.java index ee341dd..87e5cc6 100644 --- a/src/main/java/turniplabs/halplibe/util/toml/Toml.java +++ b/src/main/java/turniplabs/halplibe/util/toml/Toml.java @@ -209,7 +209,12 @@ public void merge(boolean complete, Toml parsed) { for (String orderedKey : parsed.orderedKeys) { if (orderedKey.startsWith(".")) { orderedKey = orderedKey.substring(1); - categories.get(orderedKey).merge(complete, parsed.categories.get(orderedKey)); + if (complete) { + if (!categories.containsKey(orderedKey)) + addCategory(orderedKey); + categories.get(orderedKey).merge(complete, parsed.categories.get(orderedKey)); + } else if (categories.containsKey(orderedKey)) + categories.get(orderedKey).merge(complete, parsed.categories.get(orderedKey)); } else { if (complete) { if (entries.containsKey(orderedKey)) { @@ -255,6 +260,8 @@ public void addMissing(Toml other) { } public void remove(String s) { + immutKeys = null; + if (s.startsWith(".")) { if (s.substring(1).contains(".")) { categories.get(s.substring(1).split("\\.")[0])