Split server and client modules and rename project to Saltatory

This commit is contained in:
ms2k 2026-10-18 20:33:45 +09:00
commit eccb914dcb
81 changed files with 160 additions and 146 deletions

6
.gitignore vendored
View file

@ -1,6 +1,6 @@
/.gradle/ **/.gradle/
/build/ **/build/
/runs/ **/runs/
/.idea/ /.idea/
*.iml *.iml
/docs/journal /docs/journal

View file

@ -1,5 +1,5 @@
# License # License
mc-tune is licensed under the GNU Lesser General Public License, version 3 only (`LGPL-3.0-only`). Copyright © 2026 mc-tune contributors. saltatory is licensed under the GNU Lesser General Public License, version 3 only (`LGPL-3.0-only`). Copyright © 2026 saltatory contributors.
The license consists of the additional permissions in [COPYING.LESSER](COPYING.LESSER) together with the GNU General Public License, version 3, in [COPYING](COPYING). Upstream attribution and modification notices appear in [NOTICE.md](NOTICE.md). The license consists of the additional permissions in [COPYING.LESSER](COPYING.LESSER) together with the GNU General Public License, version 3, in [COPYING](COPYING). Upstream attribution and modification notices appear in [NOTICE.md](NOTICE.md).

View file

@ -1,6 +1,6 @@
# Third-party notices # Third-party notices
| Upstream | License | Contribution to mc-tune | | Upstream | License | Contribution to saltatory |
| ----------------------------------------------------------------------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ----------------------------------------------------------------------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Almanac 1.5.2](https://modrinth.com/mod/almanac), by frikinjay | LGPL-3.0-only | `AlmanacMixin` adapts `ItemNBTUtil` behavior from the released JAR's bytecode. Changes dated 2026-10-09 remove NBT copies and bypass normalization callbacks when custom data cannot change. | | [Almanac 1.5.2](https://modrinth.com/mod/almanac), by frikinjay | LGPL-3.0-only | `AlmanacMixin` adapts `ItemNBTUtil` behavior from the released JAR's bytecode. Changes dated 2026-10-09 remove NBT copies and bypass normalization callbacks when custom data cannot change. |
| [Mekanism 10.7.19.85](https://github.com/mekanism/Mekanism/tree/a00109e4856fd38b9c5b3dd7f22ce4a59cd65a80) | MIT | `SlotMemo`, `FlowBatch`, `InvMixin`, and `MekMixin` adapt `TransporterManager` inventory prediction. Changes dated 2026-10-09 memoize slot validation within one prediction snapshot and eliminate a repeated stack compatibility check. | | [Mekanism 10.7.19.85](https://github.com/mekanism/Mekanism/tree/a00109e4856fd38b9c5b3dd7f22ce4a59cd65a80) | MIT | `SlotMemo`, `FlowBatch`, `InvMixin`, and `MekMixin` adapt `TransporterManager` inventory prediction. Changes dated 2026-10-09 memoize slot validation within one prediction snapshot and eliminate a repeated stack compatibility check. |

View file

@ -1,11 +1,12 @@
# mc-tune # Saltatory
A small server-side modmod that reduces inventory, crafting and NBT overhead in a personal modded Minecraft server. Small modmods for personal modded Minecraft server and client tuning.
Targets Minecraft 1.21.1 on GraalVM CE 25. Local builds require JDK 25 and Gradle 9.1.0. Targets Minecraft 1.21.1 on GraalVM CE 25. Local builds require JDK 25 and Gradle 9.1.0.
```sh ```sh
gradle --no-daemon format build gradle --no-daemon :server:format :server:build
gradle --no-daemon :client:format :client:build
``` ```
[License](LICENSE.md) · [Third-party notices](NOTICE.md) [License](LICENSE.md) · [Third-party notices](NOTICE.md)

View file

@ -1,74 +1,35 @@
plugins { plugins {
id 'java-library' id 'base'
id 'checkstyle' id 'net.neoforged.gradle.userdev' version '7.1.39' apply false
id 'net.neoforged.gradle.userdev' version '7.1.39'
} }
group = 'dev.quorate' subprojects { mod ->
version = '0.42.1' apply plugin: 'java-library'
def modVersion = version.toString() apply plugin: 'checkstyle'
apply plugin: 'net.neoforged.gradle.userdev'
group = 'dev.quorate'
java { java {
toolchain.languageVersion = JavaLanguageVersion.of(25) toolchain.languageVersion = JavaLanguageVersion.of(25)
withSourcesJar() withSourcesJar()
} }
repositories { mavenCentral() }
repositories {
ivy {
url = 'https://github.com/tlaplus/tlaplus/releases/download'
patternLayout { artifact '[revision]/[artifact].[ext]' }
metadataSources { artifact() }
content { includeModule 'org.lamport', 'tla2tools' }
}
mavenCentral()
maven {
url = 'https://maven.blamejared.com/'
content { includeGroup 'com.hrznstudio' }
}
maven {
url = 'https://modmaven.dev/'
content { includeGroup 'mekanism' }
}
maven {
url = 'https://www.cursemaven.com'
content { includeGroup 'curse.maven' }
}
maven {
url = 'https://api.modrinth.com/maven'
content { includeGroup 'maven.modrinth' }
}
}
sourceSets {
probe {
compileClasspath += sourceSets.main.output + configurations.compileClasspath
}
}
configurations { javafmt } configurations { javafmt }
dependencies { dependencies {
implementation 'net.neoforged:neoforge:21.1.250' implementation 'net.neoforged:neoforge:21.1.250'
compileOnly('mekanism:Mekanism:1.21.1-10.7.19.85') { transitive = false }
compileOnly('curse.maven:functional-storage-556861:8459097') { transitive = false }
compileOnly('maven.modrinth:refined-storage:lHHiI26k') { transitive = false }
compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'org.apiguardian:apiguardian-api:1.1.2'
compileOnly 'io.github.llamalad7:mixinextras-neoforge:0.5.3'
probeCompileOnly('com.hrznstudio:titanium:1.21-4.0.43') { transitive = false }
testImplementation 'it.unimi.dsi:fastutil:8.5.12'
testImplementation('org.lamport:tla2tools:v1.7.4') { transitive = false }
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4' testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.4'
javafmt('com.google.googlejavaformat:google-java-format:1.37.0:all-deps') { transitive = false } javafmt('com.google.googlejavaformat:google-java-format:1.37.0:all-deps') { transitive = false }
} }
checkstyle { checkstyle {
toolVersion = '14.3.0' toolVersion = '14.3.0'
configFile = file('quality.xml') configFile = rootProject.file('quality.xml')
} }
def javaSources = fileTree('src') { include '**/*.java' } def javaSources = fileTree('src') { include '**/*.java' }
['format': ['--replace'], 'formatCheck': ['--dry-run', '--set-exit-if-changed']].each { name, mode -> ['format': ['--replace'], 'formatCheck': ['--dry-run', '--set-exit-if-changed']].each { name, mode ->
tasks.register(name, JavaExec) { tasks.register(name, JavaExec) {
inputs.files(javaSources).skipWhenEmpty()
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) } javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) }
classpath = configurations.javafmt classpath = configurations.javafmt
mainClass = 'com.google.googlejavaformat.java.Main' mainClass = 'com.google.googlejavaformat.java.Main'
@ -80,61 +41,33 @@ def javaSources = fileTree('src') { include '**/*.java' }
} }
tasks.named('formatCheck') { mustRunAfter 'format' } tasks.named('formatCheck') { mustRunAfter 'format' }
tasks.withType(Checkstyle).configureEach { mustRunAfter 'format' } tasks.withType(Checkstyle).configureEach { mustRunAfter 'format' }
tasks.register('quality') { dependsOn 'formatCheck', 'checkstyleMain', 'checkstyleTest', 'checkstyleProbe' } tasks.register('quality') { dependsOn 'formatCheck', 'checkstyleMain', 'checkstyleTest' }
tasks.named('check') { dependsOn 'quality' } tasks.named('check') { dependsOn 'quality' }
tasks.named('checkstyleProbe') {
dependsOn 'compileProbeJava'
classpath = sourceSets.probe.compileClasspath
}
tasks.named('test') { tasks.named('test') {
dependsOn 'quality' dependsOn 'quality'
inputs.dir('src/test/tla')
useJUnitPlatform() useJUnitPlatform()
maxHeapSize = '512m' maxHeapSize = '512m'
jvmArgs '-XX:+UseZGC' jvmArgs '-XX:+UseZGC'
} }
tasks.named('processResources') { tasks.named('processResources') {
inputs.property 'version', modVersion inputs.property 'version', provider { mod.version.toString() }
filesMatching('META-INF/neoforge.mods.toml') { expand version: modVersion } filesMatching('META-INF/neoforge.mods.toml') { expand version: mod.version.toString() }
} }
tasks.withType(JavaCompile).matching { it.name in ['compileJava', 'compileTestJava', 'compileProbeJava'] }.configureEach {
['compileJava', 'compileTestJava', 'compileProbeJava'].each { name ->
tasks.named(name, JavaCompile) {
options.encoding = 'UTF-8' options.encoding = 'UTF-8'
options.compilerArgs += ['-Xlint:all', '-Werror', '-proc:none'] options.compilerArgs += ['-Xlint:all', '-Werror', '-proc:none']
mustRunAfter 'format' mustRunAfter 'format'
} }
}
tasks.withType(Jar).configureEach { tasks.withType(Jar).configureEach {
preserveFileTimestamps = false preserveFileTimestamps = false
reproducibleFileOrder = true reproducibleFileOrder = true
from(['LICENSE.md', 'COPYING', 'COPYING.LESSER', 'NOTICE.md']) { into 'META-INF' } from(['LICENSE.md', 'COPYING', 'COPYING.LESSER', 'NOTICE.md'].collect { rootProject.file(it) }) { into 'META-INF' }
}
tasks.named('processProbeResources') {
inputs.property 'version', modVersion
filesMatching('META-INF/neoforge.mods.toml') { expand version: modVersion }
dependsOn 'test'
['Slots', 'SlotOverflow', 'Traits'].each { name ->
from(layout.buildDirectory.dir("reports/models/${name}")) {
include 'cases.jsonl', 'stats.json'
eachFile { entry -> entry.path = "cases/${name}." + (entry.name == 'cases.jsonl' ? 'jsonl' : 'json') }
}
}
from({ zipTree(configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts.find {
it.moduleVersion.id.group == 'mekanism' && it.name == 'Mekanism'
}.file) }) {
include 'mekanism/common/content/transporter/TransporterManager*.class'
into 'baseline'
} }
} }
tasks.register('probeJar', Jar) { ['format', 'formatCheck', 'quality'].each { name ->
dependsOn 'quality', 'probeClasses' tasks.register(name) { dependsOn subprojects.collect { it.tasks.named(name) } }
archiveBaseName = 'mc-checks' }
destinationDirectory = layout.buildDirectory.dir('probe') ['assemble', 'check'].each { name ->
from sourceSets.probe.output tasks.named(name) { dependsOn subprojects.collect { it.tasks.named(name) } }
} }

2
client/build.gradle Normal file
View file

@ -0,0 +1,2 @@
version = '0.1.0'
base.archivesName = 'saltatory-client'

77
server/build.gradle Normal file
View file

@ -0,0 +1,77 @@
version = '0.42.1'
base.archivesName = 'saltatory'
repositories {
ivy {
url = 'https://github.com/tlaplus/tlaplus/releases/download'
patternLayout { artifact '[revision]/[artifact].[ext]' }
metadataSources { artifact() }
content { includeModule 'org.lamport', 'tla2tools' }
}
maven {
url = 'https://maven.blamejared.com/'
content { includeGroup 'com.hrznstudio' }
}
maven {
url = 'https://modmaven.dev/'
content { includeGroup 'mekanism' }
}
maven {
url = 'https://www.cursemaven.com'
content { includeGroup 'curse.maven' }
}
maven {
url = 'https://api.modrinth.com/maven'
content { includeGroup 'maven.modrinth' }
}
}
sourceSets {
probe {
compileClasspath += sourceSets.main.output + configurations.compileClasspath
}
}
dependencies {
compileOnly('mekanism:Mekanism:1.21.1-10.7.19.85') { transitive = false }
compileOnly('curse.maven:functional-storage-556861:8459097') { transitive = false }
compileOnly('maven.modrinth:refined-storage:lHHiI26k') { transitive = false }
compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'org.apiguardian:apiguardian-api:1.1.2'
compileOnly 'io.github.llamalad7:mixinextras-neoforge:0.5.3'
probeCompileOnly('com.hrznstudio:titanium:1.21-4.0.43') { transitive = false }
testImplementation 'it.unimi.dsi:fastutil:8.5.12'
testImplementation('org.lamport:tla2tools:v1.7.4') { transitive = false }
}
tasks.named('quality') { dependsOn 'checkstyleProbe' }
tasks.named('checkstyleProbe') {
dependsOn 'compileProbeJava'
classpath = sourceSets.probe.compileClasspath
}
tasks.named('test') { inputs.dir('src/test/tla') }
tasks.named('processProbeResources') {
inputs.property 'version', provider { project.version.toString() }
filesMatching('META-INF/neoforge.mods.toml') { expand version: project.version.toString() }
dependsOn 'test'
['Slots', 'SlotOverflow', 'Traits'].each { name ->
from(layout.buildDirectory.dir("reports/models/${name}")) {
include 'cases.jsonl', 'stats.json'
eachFile { entry -> entry.path = "cases/${name}." + (entry.name == 'cases.jsonl' ? 'jsonl' : 'json') }
}
}
from({ zipTree(configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts.find {
it.moduleVersion.id.group == 'mekanism' && it.name == 'Mekanism'
}.file) }) {
include 'mekanism/common/content/transporter/TransporterManager*.class'
into 'baseline'
}
}
tasks.register('probeJar', Jar) {
dependsOn 'quality', 'probeClasses'
archiveBaseName = 'saltatory-checks'
destinationDirectory = layout.buildDirectory.dir('probe')
from sourceSets.probe.output
}

View file

@ -0,0 +1,29 @@
package dev.quorate.tune;
import it.unimi.dsi.fastutil.HashCommon;
import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap;
import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
public final class ComponentMap<K, V> extends Reference2ObjectLinkedOpenHashMap<K, V> {
private static final long serialVersionUID = 1L;
// Deserialized maps use the original lookup because key identities change.
private final transient K hot;
private final transient int hash;
public ComponentMap(Reference2ObjectMap<K, V> map, K hot) {
super(map);
this.hot = hot;
hash = HashCommon.mix(System.identityHashCode(hot));
}
@Override
public V get(Object component) {
if (component != null && component == hot) {
int slot = hash & mask;
var stored = key[slot];
if (stored == component) return value[slot];
if (stored == null) return defRetValue;
}
return super.get(component);
}
}

View file

@ -0,0 +1,154 @@
package dev.quorate.tune;
import com.buuz135.functionalstorage.inventory.ControllerInventoryHandler;
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import mekanism.common.content.transporter.TransporterStack;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.items.IItemHandler;
import java.util.Arrays;
import java.util.Set;
public final class FlowBatch {
private static final class State {
private final IItemHandler handler;
private final ItemStack[] inventory;
private final int[] sizes;
private final int[] limits;
private final boolean[] valid;
private final boolean[] certified;
private final int[] empty;
private final int emptyCount;
private final Reference2IntOpenHashMap<Item> first;
private Reference2ObjectOpenHashMap<Item, long[]> denied;
private State(IItemHandler handler, ItemStack[] inventory, int[] sizes) {
this.handler = handler;
this.inventory = inventory.clone();
this.sizes = sizes.clone();
limits = new int[sizes.length];
Arrays.fill(limits, -1);
valid = new boolean[sizes.length];
certified = new boolean[sizes.length];
empty = new int[sizes.length];
first = new Reference2IntOpenHashMap<>(sizes.length);
first.defaultReturnValue(-1);
int count = 0;
for (int slot = 0; slot < sizes.length; slot++) {
if (sizes[slot] == 0) empty[count++] = slot;
else first.putIfAbsent(inventory[slot].getItem(), slot);
}
emptyCount = count;
}
private int limit(int slot) {
if (limits[slot] == -1) limits[slot] = handler.getSlotLimit(slot);
return limits[slot];
}
private boolean rejected(int ordinal, Item item, int count) {
if (denied == null) return false;
var masks = denied.get(item);
return masks != null && (masks[ordinal] & 1L << (count - 1)) != 0;
}
private void reject(int ordinal, Item item, int count) {
if (denied == null) denied = new Reference2ObjectOpenHashMap<>();
denied.computeIfAbsent(item, _ -> new long[emptyCount])[ordinal] |= 1L << (count - 1);
}
private boolean insert(ItemStack stack, int count) {
var item = stack.getItem();
int target = first.getInt(item);
for (int ordinal = 0; ordinal < emptyCount; ordinal++) {
int slot = empty[ordinal];
if (target >= 0 && slot >= target) break;
if (sizes[slot] > 0 || rejected(ordinal, item, count)) continue;
int limit = limit(slot);
if (limit == 0) continue;
if (limit < 0) return false;
if (!handler.isItemValid(slot, stack)) {
reject(ordinal, item, count);
continue;
}
var remainder = handler.insertItem(slot, stack, true);
if (remainder == ItemStack.EMPTY) {
if (count > limit) return false;
inventory[slot] = stack;
sizes[slot] = count;
first.put(item, slot);
valid[slot] = true;
return true;
}
if (remainder.getCount() == count) {
reject(ordinal, item, count);
continue;
}
return false;
}
if (target < 0) return false;
if (!plain(inventory[target])) return false;
if (!valid[target]) {
if (!handler.isItemValid(target, stack)) return false;
valid[target] = true;
}
int limit = limit(target);
long merged = (long) sizes[target] + count;
if (limit <= 0) return false;
if (merged > limit) return false;
if (limit > 64 && merged > 64 && !certified[target]) {
if (handler.insertItem(target, stack.copyWithCount(65), true) != ItemStack.EMPTY)
return false;
certified[target] = true;
}
sizes[target] = (int) merged;
return true;
}
private void commit(ItemStack[] target, int[] counts) {
System.arraycopy(inventory, 0, target, 0, target.length);
System.arraycopy(sizes, 0, counts, 0, counts.length);
}
}
private FlowBatch() {}
private static boolean known(IItemHandler handler) {
if (handler instanceof ControllerInventoryHandler controller)
return handler.getClass()
.getName()
.equals(
"com.buuz135.functionalstorage.block.tile.StorageControllerTile$1")
&& controller.getDrawers().getItemHandlers().stream().allMatch(Handlers::leaf);
return Handlers.leaf(handler);
}
private static boolean plain(ItemStack stack) {
return stack.getComponentsPatch() == DataComponentPatch.EMPTY
&& stack.get(DataComponents.CUSTOM_DATA) == null;
}
public static boolean apply(
IItemHandler handler, ItemStack[] inventory, int[] sizes, Set<TransporterStack> flows) {
if (!known(handler)) return false;
for (int size : sizes) if (size < 0) return false;
var state = new State(handler, inventory, sizes);
for (var flow : flows) {
if (flow == null || !flow.getPathType().hasTarget()) continue;
var stack = flow.itemStack;
if (!plain(stack)) return false;
int count = stack.getCount();
if (count <= 0 || count > 64 || stack.getMaxStackSize() != 64) return false;
if (!state.insert(stack, count)) return false;
}
state.commit(inventory, sizes);
return true;
}
}

View file

@ -0,0 +1,32 @@
package dev.quorate.tune;
import com.buuz135.functionalstorage.inventory.ControllerInventoryHandler;
import net.neoforged.neoforge.items.IItemHandler;
public final class Handlers {
private Handlers() {}
public static boolean known(IItemHandler handler) {
if (handler instanceof ControllerInventoryHandler controller) {
if (!handler.getClass()
.getName()
.equals("com.buuz135.functionalstorage.block.tile.StorageControllerTile$1"))
return false;
return controller.getDrawers().getItemHandlers().stream()
.allMatch(
member -> member instanceof ControllerInventoryHandler || leaf(member));
}
return leaf(handler);
}
static boolean leaf(IItemHandler handler) {
return switch (handler.getClass().getName()) {
case "com.buuz135.functionalstorage.block.tile.DrawerTile$1",
"com.buuz135.functionalstorage.block.tile.CompactingDrawerTile$1",
"com.buuz135.functionalstorage.block.tile.SimpleCompactingDrawerTile$1" ->
true;
default -> false;
};
}
}

View file

@ -0,0 +1,71 @@
package dev.quorate.tune;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.RandomAccess;
public final class MemberList<E> extends AbstractList<E> implements RandomAccess {
private final ArrayList<E> entries;
private final Map<E, Integer> counts = new HashMap<>();
public MemberList(Collection<? extends E> entries) {
this.entries = new ArrayList<>(entries);
for (var entry : entries) addCount(entry);
}
@Override
public E get(int index) {
return entries.get(index);
}
@Override
public int size() {
return entries.size();
}
@Override
public boolean contains(Object value) {
return counts.containsKey(value);
}
@Override
public void add(int index, E value) {
entries.add(index, value);
addCount(value);
modCount++;
}
@Override
public E set(int index, E value) {
E old = entries.set(index, value);
removeCount(old);
addCount(value);
return old;
}
@Override
public E remove(int index) {
E old = entries.remove(index);
removeCount(old);
modCount++;
return old;
}
@Override
public void clear() {
entries.clear();
counts.clear();
modCount++;
}
private void addCount(E value) {
counts.merge(value, 1, Integer::sum);
}
private void removeCount(E value) {
counts.compute(value, (key, count) -> count == 1 ? null : count - 1);
}
}

View file

@ -0,0 +1,82 @@
package dev.quorate.tune;
import net.minecraft.nbt.ByteArrayTag;
import net.minecraft.nbt.ByteTag;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.DoubleTag;
import net.minecraft.nbt.EndTag;
import net.minecraft.nbt.FloatTag;
import net.minecraft.nbt.IntArrayTag;
import net.minecraft.nbt.IntTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.LongArrayTag;
import net.minecraft.nbt.LongTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.ShortTag;
import net.minecraft.nbt.StringTag;
import net.minecraft.nbt.Tag;
import java.util.ArrayList;
public final class NbtTree {
private NbtTree() {}
public static Tag convert(Tag input) {
if (input == null) return null;
// Tag subclasses may override copying; their encoding stays with the native codec.
var type = input.getClass();
return switch (input) {
case CompoundTag compound when type == CompoundTag.class -> {
var result = new CompoundTag();
for (var name : compound.getAllKeys()) {
if (name == null) yield null;
var value = convert(compound.get(name));
if (value == null) yield null;
result.put(name, value);
}
yield result;
}
case ListTag list when type == ListTag.class -> {
var entries = new ArrayList<Tag>(list.size());
for (var element : list) {
var value = element;
if (list.getElementType() == Tag.TAG_COMPOUND) {
if (element == null || element.getClass() != CompoundTag.class) yield null;
var compound = (CompoundTag) element;
if (compound.size() == 1 && compound.get("") != null)
value = compound.get("");
}
var converted = convert(value);
if (converted == null) yield null;
entries.add(converted);
}
yield NbtOps.INSTANCE.createList(entries.stream());
}
case ByteArrayTag array when type == ByteArrayTag.class -> {
var data = array.getAsByteArray();
yield data == null ? null : new ByteArrayTag(data.clone());
}
case IntArrayTag array when type == IntArrayTag.class -> {
var data = array.getAsIntArray();
yield data == null ? null : new IntArrayTag(data.clone());
}
case LongArrayTag array when type == LongArrayTag.class -> {
var data = array.getAsLongArray();
yield data == null ? null : new LongArrayTag(data.clone());
}
case ByteTag number when type == ByteTag.class -> ByteTag.valueOf(number.getAsByte());
case ShortTag number when type == ShortTag.class ->
ShortTag.valueOf(number.getAsShort());
case IntTag number when type == IntTag.class -> IntTag.valueOf(number.getAsInt());
case LongTag number when type == LongTag.class -> LongTag.valueOf(number.getAsLong());
case FloatTag number when type == FloatTag.class ->
FloatTag.valueOf(number.getAsFloat());
case DoubleTag number when type == DoubleTag.class ->
DoubleTag.valueOf(number.getAsDouble());
case StringTag string when type == StringTag.class ->
StringTag.valueOf(string.getAsString());
case EndTag _ when type == EndTag.class -> EndTag.INSTANCE;
default -> null;
};
}
}

View file

@ -0,0 +1,20 @@
package dev.quorate.tune;
import java.util.LinkedHashMap;
import java.util.Map;
public final class PlanRows<K> {
private boolean shared;
public void share() {
shared = true;
}
public void prepare(Map<Integer, Map<K, Long>> rows) {
if (!shared) return;
for (var entry : rows.entrySet()) {
entry.setValue(new LinkedHashMap<>(entry.getValue()));
}
shared = false;
}
}

View file

@ -0,0 +1,5 @@
package dev.quorate.tune;
public interface PlanShare {
void mctune$share();
}

View file

@ -0,0 +1,24 @@
package dev.quorate.tune;
import mekanism.common.content.transporter.TransporterStack;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.items.IItemHandler;
import java.util.Set;
public interface Prediction {
boolean mctune$batch(IItemHandler handler, Set<TransporterStack> flows);
boolean mctune$check(IItemHandler handler, int slot, ItemStack stack, boolean inFlight);
boolean mctune$matches(ItemStack left, ItemStack right);
int mctune$next(int slot, ItemStack stack, int count);
boolean mctune$begin(ItemStack stack, int count, boolean inFlight);
void mctune$finish(ItemStack stack, int count, boolean inFlight, int left);
ItemStack mctune$simulate(IItemHandler handler, int slot, ItemStack stack, boolean simulate);
}

View file

@ -0,0 +1,54 @@
package dev.quorate.tune;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class SimCache<K> {
private record Result<K>(K item, int count, int left) {}
private final Object[] items;
private final int[] counts;
private final int[] remaining;
private final List<List<Result<K>>> extra;
public SimCache(int slots) {
items = new Object[slots];
counts = new int[slots];
remaining = new int[slots];
extra = new ArrayList<>(Collections.nCopies(slots, null));
}
public int get(int slot, K item, int count) {
if (items[slot] == item && counts[slot] == count) return remaining[slot];
var results = extra.get(slot);
if (results != null)
for (var result : results) {
if (result.item() == item && result.count() == count) return result.left();
}
return -1;
}
public void put(int slot, K item, int count, int left) {
if (items[slot] == null || items[slot] == item && counts[slot] == count) {
items[slot] = item;
counts[slot] = count;
remaining[slot] = left;
return;
}
var results = extra.get(slot);
if (results == null) {
results = new ArrayList<>();
extra.set(slot, results);
}
var entry = new Result<>(item, count, left);
for (int index = 0; index < results.size(); index++) {
var result = results.get(index);
if (result.item() == item && result.count() == count) {
results.set(index, entry);
return;
}
}
results.add(entry);
}
}

View file

@ -0,0 +1,310 @@
package dev.quorate.tune;
import mekanism.common.util.InventoryUtils;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponents;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.neoforged.neoforge.items.IItemHandler;
import java.util.BitSet;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
public final class SlotMemo {
private static final int VALID = 1;
private static final int INVALID = 2;
private static final int MATCH = 4;
private static final int DIFFERENT = 8;
private record StackKey(Item item, DataComponentPatch data, boolean empty) {}
private record AppendRule(int slot, int limit, int maxStack, ItemStack stack) {}
private static final class CountState {
private final byte[] valid;
private final BitSet slots;
private AppendRule line;
private CountState(int size, BitSet slots) {
valid = new byte[size];
this.slots = slots;
}
}
private record StackState(
StackKey key, byte[] matches, BitSet slots, Map<Integer, CountState> rows) {}
// InventoryInfo confines cached decisions to one TransporterManager prediction snapshot.
private final ItemStack[] inventory;
private final int[] sizes;
private final int[] limits;
private final int[] actual;
private final SimCache<Item> trials;
private final Item[] kinds;
private final boolean[] compared;
private final BitSet eligible;
private final BitSet universal;
private final Map<Item, BitSet> indices;
private final Map<StackKey, StackState> types = new HashMap<>();
private int offered = -1;
private ItemStack before;
private int beforeSize;
private int capacity;
private boolean stable;
private boolean changed;
private StackState type;
private CountState row;
private ItemStack selected;
private int selectedCount;
public SlotMemo(
ItemStack[] inventory, int[] sizes, int[] actual, int[] limits, boolean inFlight) {
indices = inFlight ? new IdentityHashMap<>() : null;
this.inventory = inventory;
this.sizes = sizes;
this.limits = limits;
this.actual = actual;
trials = new SimCache<>(inventory.length);
kinds = new Item[inventory.length];
compared = new boolean[inventory.length];
eligible = new BitSet(inventory.length);
eligible.set(0, inventory.length);
universal = new BitSet(inventory.length);
for (int slot = 0; slot < inventory.length; slot++) {
var item = sizes[slot] > 0 && actual[slot] > 0 ? inventory[slot].getItem() : null;
if (item == Items.AIR) item = null;
kinds[slot] = item;
if (item == null) universal.set(slot);
else if (indices != null) index(item).set(slot);
}
}
private BitSet index(Item item) {
var slots = indices.get(item);
if (slots == null) {
slots = new BitSet(inventory.length);
indices.put(item, slots);
}
return slots;
}
private StackState create(StackKey key) {
var slots = key.empty() ? (BitSet) eligible.clone() : (BitSet) universal.clone();
if (!key.empty()) {
if (indices == null) {
for (int slot = 0; slot < kinds.length; slot++)
if (kinds[slot] == key.item()) slots.set(slot);
} else {
var matching = indices.get(key.item());
if (matching != null) slots.or(matching);
}
}
slots.and(eligible);
return new StackState(key, new byte[inventory.length], slots, HashMap.newHashMap(2));
}
private void select(ItemStack stack) {
if (selected == stack) return;
int count = stack.getCount();
var item = stack.getItem();
var data = stack.getComponentsPatch();
boolean empty = count <= 0 || item == Items.AIR;
boolean changed =
data != DataComponentPatch.EMPTY
|| type == null
|| type.key().data() != data
|| type.key().item() != item
|| type.key().empty() != empty;
if (changed) {
var key = new StackKey(item, data, empty);
type = types.get(key);
if (type == null) {
type = create(key);
types.put(key, type);
}
}
if (changed || selectedCount != count) {
row = type.rows().get(count);
if (row == null) {
row = new CountState(inventory.length, (BitSet) type.slots().clone());
type.rows().put(count, row);
}
}
selectedCount = count;
selected = stack;
}
public boolean check(IItemHandler handler, int slot, ItemStack stack, int limit) {
offer(slot);
int size = sizes[slot];
if (size > 0 && size >= limit) {
if (limit != -1) exclude(slot);
return false;
}
if (size <= 0 && compared[slot]) reset(slot);
select(stack);
if (!row.slots.get(slot)) return false;
if (size > 0) {
int state = type.matches()[slot];
if (state == 0) {
state =
InventoryUtils.areItemsStackable(inventory[slot], stack)
? MATCH
: DIFFERENT;
type.matches()[slot] = (byte) state;
compared[slot] = true;
}
if (state == DIFFERENT) {
type.slots().clear(slot);
for (var value : type.rows().values()) value.slots.clear(slot);
return false;
}
}
int valid = row.valid[slot];
if (valid == 0) {
valid = handler.isItemValid(slot, stack) ? VALID : INVALID;
row.valid[slot] = (byte) valid;
}
if (valid == INVALID) row.slots.clear(slot);
return valid == VALID;
}
private void flush() {
if (offered < 0) return;
int slot = offered;
offered = -1;
changed |= sizes[slot] != beforeSize;
int limit = limits[slot];
if (limit != -1 && (limit == 0 || sizes[slot] > 0 && sizes[slot] >= limit)) {
exclude(slot);
}
refresh(slot);
}
public int next(int start, ItemStack stack, int count) {
flush();
if (count == 0 || start >= inventory.length) return start;
if (selected != stack
&& stack.getComponentsPatch() == DataComponentPatch.EMPTY
&& stack.get(DataComponents.CUSTOM_DATA) == null) select(stack);
var slots = selected == stack ? row.slots : eligible;
int slot = slots.nextSetBit(start);
if (slot >= 0) offer(slot);
return slot < 0 ? inventory.length : slot;
}
private static boolean plain(ItemStack stack) {
return stack.getComponentsPatch() == DataComponentPatch.EMPTY
&& stack.get(DataComponents.CUSTOM_DATA) == null;
}
public boolean begin(ItemStack stack, int count, boolean inFlight) {
changed = false;
if (!inFlight || count <= 0 || !plain(stack)) return false;
select(stack);
var line = row.line;
if (line == null
|| selectedCount != count
|| row.slots.nextSetBit(0) != line.slot()
|| inventory[line.slot()] != line.stack()
|| sizes[line.slot()] <= line.maxStack()
|| (long) sizes[line.slot()] + count > line.limit()) return false;
sizes[line.slot()] += count;
if (sizes[line.slot()] >= limits[line.slot()]) exclude(line.slot());
return true;
}
public void finish(ItemStack stack, int count, boolean inFlight, int left) {
if (inFlight
&& left == 0
&& !changed
&& offered >= 0
&& stable
&& count > 0
&& selected == stack
&& selectedCount == count
&& beforeSize > stack.getMaxStackSize()
&& (long) sizes[offered] - beforeSize == count
&& row.slots.nextSetBit(0) == offered) {
row.line =
new AppendRule(offered, capacity, stack.getMaxStackSize(), inventory[offered]);
}
flush();
}
public ItemStack simulate(IItemHandler handler, int slot, ItemStack stack, boolean simulate) {
if (!plain(stack)) return handler.insertItem(slot, stack, simulate);
int count = stack.getCount();
var item = stack.getItem();
int left = trials.get(slot, item, count);
ItemStack result;
if (left < 0) {
result = handler.insertItem(slot, stack, simulate);
left = result.getCount();
trials.put(slot, item, count, left);
} else
result =
left == count ? stack : left == 0 ? ItemStack.EMPTY : stack.copyWithCount(left);
int accepted = count - left;
capacity =
accepted < count ? Math.min(limits[slot], actual[slot] + accepted) : limits[slot];
stable = true;
return result;
}
private void offer(int slot) {
offered = slot;
before = inventory[slot];
beforeSize = sizes[slot];
stable = false;
}
private void refresh(int slot) {
var previous = kinds[slot];
Item current = previous;
if (sizes[slot] <= 0) current = null;
else if (inventory[slot] != before || previous == null) {
var stack = inventory[slot];
current = stack.isEmpty() ? null : stack.getItem();
}
if (current != previous) {
if (previous == null) universal.clear(slot);
else if (indices != null) indices.get(previous).clear(slot);
if (current == null) universal.set(slot);
else if (indices != null) index(current).set(slot);
kinds[slot] = current;
reset(slot);
} else if (sizes[slot] <= 0 && compared[slot]) reset(slot);
}
private void exclude(int slot) {
if (!eligible.get(slot)) return;
eligible.clear(slot);
for (var value : types.values()) {
value.slots().clear(slot);
for (var entry : value.rows().values()) entry.slots.clear(slot);
}
}
private void reset(int slot) {
// Overflow can empty a virtual slot and permit assignment to another item.
for (var value : types.values()) {
value.matches()[slot] = 0;
boolean allowed =
eligible.get(slot)
&& (value.key().empty()
|| kinds[slot] == null
|| value.key().item() == kinds[slot]);
value.slots().set(slot, allowed);
for (var entry : value.rows().values()) {
entry.slots.set(slot, allowed && entry.valid[slot] != INVALID);
entry.line = null;
}
}
compared[slot] = false;
}
}

View file

@ -0,0 +1,42 @@
package dev.quorate.tune;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.Dynamic;
import com.mojang.serialization.DynamicOps;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.resources.RegistryOps;
public final class TreeCodec implements Codec<CompoundTag> {
private final Codec<CompoundTag> original;
public TreeCodec(Codec<CompoundTag> original) {
this.original = original;
}
@Override
public <T> DataResult<Pair<CompoundTag, T>> decode(DynamicOps<T> ops, T input) {
return original.decode(ops, input);
}
@Override
@SuppressWarnings("unchecked")
public <T> DataResult<T> encode(CompoundTag input, DynamicOps<T> ops, T prefix) {
if (ops.getClass() == RegistryOps.class
&& ops instanceof RegistryOps<?> registry
&& registry.withParent(NbtOps.INSTANCE) == registry) {
var converted = NbtTree.convert(input);
if (converted != null)
return Codec.PASSTHROUGH.encode(new Dynamic<>(ops, (T) converted), ops, prefix);
}
return original.encode(input, ops, prefix);
}
@Override
public String toString() {
return original.toString();
}
}

View file

@ -0,0 +1,6 @@
package dev.quorate.tune;
import net.neoforged.fml.common.Mod;
@Mod("saltatory")
public final class TuneMod {}

View file

@ -0,0 +1,39 @@
package dev.quorate.tune.mixin;
import net.minecraft.core.component.DataComponents;
import net.minecraft.world.item.ItemStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.util.Set;
@Mixin(targets = "com.frikinjay.almanac.util.ItemNBTUtil", remap = false)
public abstract class AlmanacMixin {
@Redirect(
method = "checkAndFix",
at =
@At(
value = "INVOKE",
target = "Ljava/util/Set;contains(Ljava/lang/Object;)Z",
ordinal = 0),
require = 1,
allow = 1)
private static boolean mctune$unchanged(Set<?> cache, Object item, ItemStack stack) {
var data = stack.get(DataComponents.CUSTOM_DATA);
// The exclusion branch avoids cancellation callbacks when custom data cannot change.
return data == null || !data.isEmpty() || cache.contains(item);
}
@Overwrite(remap = false)
private static boolean tryRemoveEmptyCustomData(ItemStack stack) {
var data = stack.get(DataComponents.CUSTOM_DATA);
if (data != null && data.isEmpty()) {
stack.remove(DataComponents.CUSTOM_DATA);
return true;
}
return false;
}
}

Some files were not shown because too many files have changed in this diff Show more