Input balancing

This commit is contained in:
DBotThePony 2023-06-23 23:30:40 +07:00
parent 2e37ff5de4
commit 4f7c9ea176
Signed by: DBot
GPG Key ID: DCC23B5715498507
14 changed files with 205 additions and 11 deletions

View File

@ -680,6 +680,8 @@ private fun gui(provider: MatteryLanguageProvider) {
gui("side_mode.pull", "Pull")
gui("side_mode.push", "Push")
gui("balance_inputs", "Balance input slots")
gui("sorting.default", "Default sorting")
gui("sorting.name", "Sort by name")
gui("sorting.id", "Sort by ID")

View File

@ -685,6 +685,8 @@ private fun gui(provider: MatteryLanguageProvider) {
gui("side_mode.pull", "Автоматическое вытягивание")
gui("side_mode.push", "Автоматическое выталкивание")
gui("balance_inputs", "Балансировать входные слоты")
gui("sorting.default", "Сортировка по умолчанию")
gui("sorting.name", "Сортировка по имени")
gui("sorting.id", "Сортировка по ID")

View File

@ -7,13 +7,10 @@ import net.minecraft.data.recipes.FinishedRecipe
import net.minecraft.resources.ResourceLocation
import net.minecraft.util.valueproviders.ConstantFloat
import net.minecraft.util.valueproviders.FloatProvider
import net.minecraft.util.valueproviders.UniformFloat
import net.minecraft.world.item.crafting.RecipeSerializer
import ru.dbotthepony.mc.otm.recipe.PlatePressRecipe
import ru.dbotthepony.mc.otm.recipe.PlatePressRecipeFactory
import ru.dbotthepony.mc.otm.core.set
import ru.dbotthepony.mc.otm.core.toJsonStrict
import ru.dbotthepony.mc.otm.data.getOrNull
class PlatePressFinishedRecipe(private val recipe: PlatePressRecipe) : FinishedRecipe {
override fun serializeRecipeData(it: JsonObject) {
@ -33,7 +30,7 @@ class PlatePressFinishedRecipe(private val recipe: PlatePressRecipe) : FinishedR
}
override fun getType(): RecipeSerializer<*> {
return PlatePressRecipeFactory
return PlatePressRecipe.Companion
}
override fun serializeAdvancement(): JsonObject? {
@ -74,7 +71,7 @@ class PlatePressShallowFinishedRecipe(
}
override fun getType(): RecipeSerializer<*> {
return PlatePressRecipeFactory
return PlatePressRecipe.Companion
}
override fun serializeAdvancement(): JsonObject? {

View File

@ -61,6 +61,12 @@ abstract class MatteryWorkerBlockEntity<JobType : IMachineJob>(
}
}
var balanceInputs = false
init {
savetables.bool(::balanceInputs)
}
protected open fun jobUpdated(new: JobType?, old: JobType?, id: Int) {}
protected abstract fun onJobFinish(job: JobType, id: Int): JobStatus
protected abstract fun computeNextJob(id: Int): JobContainer<JobType>

View File

@ -17,6 +17,7 @@ import ru.dbotthepony.mc.otm.capability.energy.WorkerEnergyStorage
import ru.dbotthepony.mc.otm.config.MachinesConfig
import ru.dbotthepony.mc.otm.container.MatteryContainer
import ru.dbotthepony.mc.otm.container.HandlerFilter
import ru.dbotthepony.mc.otm.container.balance
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.menu.tech.PlatePressMenu
import ru.dbotthepony.mc.otm.menu.tech.TwinPlatePressMenu
@ -87,6 +88,14 @@ class PlatePressBlockEntity(
return JobContainer.success(MachineItemJob(recipe.getResultItem(level.registryAccess()), recipe.workTime.toDouble(), BASELINE_CONSUMPTION, experience = recipe.experience.sample(level.random)))
}
override fun tick() {
if (isTwin && balanceInputs) {
inputContainer.balance()
}
super.tick()
}
companion object {
private val BASELINE_CONSUMPTION = Decimal(15)
}

View File

@ -70,9 +70,8 @@ object Widgets18 {
val REDSTONE_LOW = controlsGrid.next()
val REDSTONE_HIGH = controlsGrid.next()
init {
controlsGrid.jump()
}
val BALANCING_DISABLED = controlsGrid.next()
val BALANCING_ENABLED = controlsGrid.next()
class SideControls {
val disabled = controlsGrid.next()

View File

@ -274,11 +274,13 @@ class DeviceControls<out S : MatteryScreen<*>>(
val itemConfig: ItemConfigPlayerInput? = null,
val energyConfig: EnergyConfigPlayerInput? = null,
val fluidConfig: FluidConfigPlayerInput? = null,
val balanceInputs: BooleanInputWithFeedback? = null,
) : EditablePanel<S>(screen, parent, x = parent.width + 3f, height = 0f, width = 0f) {
val itemConfigButton: LargeRectangleButtonPanel<S>?
val energyConfigButton: LargeRectangleButtonPanel<S>?
val fluidConfigButton: LargeRectangleButtonPanel<S>?
val redstoneControlsButton: LargeEnumRectangleButtonPanel<S, RedstoneSetting>?
val balanceInputsButton: LargeBooleanRectangleButtonPanel<S>?
private var nextY = 0f
fun <P : EditablePanel<@UnsafeVariance S>> addButton(button: P): P {
@ -302,6 +304,18 @@ class DeviceControls<out S : MatteryScreen<*>>(
redstoneControlsButton = null
}
if (balanceInputs != null) {
balanceInputsButton = addButton(LargeBooleanRectangleButtonPanel(
screen, this,
prop = balanceInputs,
skinElementActive = Widgets18.BALANCING_ENABLED,
skinElementInactive = Widgets18.BALANCING_DISABLED).also {
it.tooltip = TranslatableComponent("otm.gui.balance_inputs")
})
} else {
balanceInputsButton = null
}
if (itemConfig != null) {
itemConfigButton = addButton(object : LargeRectangleButtonPanel<S>(screen, this@DeviceControls, skinElement = Widgets18.ITEMS_CONFIGURATION) {
init {
@ -375,6 +389,9 @@ fun <S : MatteryScreen<*>> makeDeviceControls(
itemConfig: ItemConfigPlayerInput? = null,
energyConfig: EnergyConfigPlayerInput? = null,
fluidConfig: FluidConfigPlayerInput? = null,
balanceInputs: BooleanInputWithFeedback? = null,
): DeviceControls<S> {
return DeviceControls(screen, parent, extra = extra, redstoneConfig = redstoneConfig, itemConfig = itemConfig, energyConfig = energyConfig, fluidConfig = fluidConfig)
return DeviceControls(screen, parent, extra = extra, redstoneConfig = redstoneConfig,
itemConfig = itemConfig, energyConfig = energyConfig, fluidConfig = fluidConfig,
balanceInputs = balanceInputs)
}

View File

@ -27,7 +27,7 @@ class TwinPlatePressScreen(menu: TwinPlatePressMenu, inventory: Inventory, title
ProgressGaugePanel(this, frame, menu.progressGauge1, 78f, PROGRESS_ARROW_TOP + 10f)
SlotPanel(this, frame, menu.outputSlots[1], 104f, PROGRESS_SLOT_TOP + 10f)
makeDeviceControls(this, frame, redstoneConfig = menu.redstoneConfig, energyConfig = menu.energyConfig, itemConfig = menu.itemConfig)
makeDeviceControls(this, frame, redstoneConfig = menu.redstoneConfig, energyConfig = menu.energyConfig, itemConfig = menu.itemConfig, balanceInputs = menu.balanceInputs)
return frame
}

View File

@ -1,11 +1,19 @@
package ru.dbotthepony.mc.otm.container
import it.unimi.dsi.fastutil.ints.IntAVLTreeSet
import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.ints.IntSet
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap
import net.minecraft.world.Container
import net.minecraft.world.item.ItemStack
import net.minecraftforge.common.capabilities.Capability
import net.minecraftforge.fluids.capability.IFluidHandler
import ru.dbotthepony.mc.otm.core.addAll
import ru.dbotthepony.mc.otm.core.collect.iterator
import ru.dbotthepony.mc.otm.core.collect.nonEmpty
import kotlin.math.roundToInt
operator fun Container.set(index: Int, value: ItemStack) = setItem(index, value)
operator fun Container.get(index: Int): ItemStack = getItem(index)
@ -109,3 +117,133 @@ inline fun Container.forEachNonEmpty(lambda: (ItemStack) -> Unit) {
lambda(value)
}
}
fun Container.balance(slots: IntSet) {
if (slots.isEmpty()) return
val empty = IntArrayList()
val itemTypes = Object2ObjectOpenCustomHashMap<ItemStack, IntAVLTreeSet>(ItemStackHashStrategy)
for (i in slots.intIterator()) {
val item = getItem(i)
if (item.isEmpty) {
empty.add(i)
} else {
itemTypes.computeIfAbsent(item, Object2ObjectFunction { IntAVLTreeSet() }).add(i)
}
}
if (itemTypes.isEmpty())
return
// только один вид предмета, просто балансируем его во все слоты
if (itemTypes.size == 1) {
val (item, list) = itemTypes.entries.first()
var count = list.stream().mapToInt { getItem(it).count }.sum()
// всего предметов меньше, чем слотов
if (count < slots.size) {
for (slot in list.intIterator()) {
getItem(slot).count = 1
}
count -= list.size
while (count > 0 && empty.isNotEmpty()) {
setItem(empty.removeInt(0), item.copyWithCount(1))
count--
}
if (count > 0) {
getItem(list.firstInt()).count += count
}
} else {
// всего предметов больше, чем слотов
val perSlot = count / slots.size
var leftover = count - perSlot * slots.size
for (i in slots.intIterator()) {
setItem(i, item.copyWithCount(perSlot))
}
for (i in slots.intIterator()) {
if (leftover <= 0) break
getItem(i).count++
leftover--
}
}
setChanged()
return
}
// а вот тут уже проблемы
// для упрощения задачи, выполним рекурсивное разбитие задачи на более простые,
// где балансировка будет происходить только между пустыми слотами и предметами одного типа
// если у нас нет пустых слотов, просто балансируем между заполненными слотами
if (empty.isEmpty) {
for (set in itemTypes.values) {
balance(set)
}
return
} else if (empty.size == 1) {
// только один пустой слот, отдадим самому "жирному" предмету
val type = itemTypes.entries.stream().max { a, b -> b.value.intStream().sum().compareTo(a.value.intStream().sum()) }.orElseThrow()
type.value.add(empty.getInt(0))
balance(type.value)
for ((a, set) in itemTypes) {
if (a !== type.key) {
balance(set)
}
}
return
}
// определяем общее количество предметов
val totalCount = itemTypes.values.stream().mapToInt { it.stream().mapToInt { getItem(it).count }.sum() }.sum().toDouble()
val totalEmpty = empty.size
// определяем доли предметов по их количеству к общему количеству,
// что позволит нам выделить претендентов на пустые слоты
for (list in itemTypes.values) {
if (empty.isEmpty) break // ошибка округления
val perc = list.stream().mapToInt { getItem(it).count }.sum() / totalCount
for (i in 0 until (perc * totalEmpty).roundToInt()) {
list.add(empty.removeInt(0))
if (empty.isEmpty) break // ошибка округления
}
}
if (empty.isNotEmpty()) {
// ошибка округления
itemTypes.values.stream().max { a, b -> b.intStream().sum().compareTo(a.intStream().sum()) }.orElseThrow().add(empty.removeInt(0))
}
for (list in itemTypes.values) {
balance(list)
}
}
fun Container.balance(slots: Iterator<Int>) {
balance(IntArraySet().also { it.addAll(slots) })
}
fun Container.balance(slots: Iterable<Int>) {
balance(IntArraySet().also { it.addAll(slots) })
}
fun Container.balance(slots: IntRange) {
balance(IntArraySet().also { it.addAll(slots) })
}
fun Container.balance(startSlot: Int = 0, endSlot: Int = containerSize - 1) {
require(startSlot <= endSlot) { "Invalid slot range: $startSlot .. $endSlot" }
balance(IntArrayList(endSlot - startSlot + 1).also { for (i in startSlot .. endSlot) it.add(i) })
}

View File

@ -0,0 +1,15 @@
package ru.dbotthepony.mc.otm.container
import it.unimi.dsi.fastutil.Hash
import net.minecraft.world.item.ItemStack
object ItemStackHashStrategy : Hash.Strategy<ItemStack> {
override fun equals(a: ItemStack?, b: ItemStack?): Boolean {
return a === b || a != null && b != null && ItemStack.isSameItemSameTags(a, b)
}
override fun hashCode(o: ItemStack?): Int {
o ?: return 0
return o.item.hashCode().xor(o.tag.hashCode())
}
}

View File

@ -7,7 +7,7 @@ import kotlin.reflect.KMutableProperty0
class BooleanInputWithFeedback(menu: MatteryMenu) : AbstractPlayerInputWithFeedback<Boolean>() {
override val input = menu.booleanInput { consumer?.invoke(it) }
override val value by menu.mSynchronizer.computedBool(BooleanSupplier { supplier?.invoke() ?: false })
override val value by menu.mSynchronizer.computedBool(BooleanSupplier { supplier?.invoke() ?: false }).property
constructor(menu: MatteryMenu, state: KMutableProperty0<Boolean>) : this(menu) {
with(state)

View File

@ -7,6 +7,7 @@ import ru.dbotthepony.mc.otm.block.entity.tech.PlatePressBlockEntity
import ru.dbotthepony.mc.otm.menu.MachineOutputSlot
import ru.dbotthepony.mc.otm.menu.MatteryPoweredMenu
import ru.dbotthepony.mc.otm.menu.MatterySlot
import ru.dbotthepony.mc.otm.menu.input.BooleanInputWithFeedback
import ru.dbotthepony.mc.otm.menu.input.EnergyConfigPlayerInput
import ru.dbotthepony.mc.otm.menu.input.ItemConfigPlayerInput
import ru.dbotthepony.mc.otm.menu.makeSlots
@ -28,6 +29,14 @@ class TwinPlatePressMenu @JvmOverloads constructor(
val energyConfig = EnergyConfigPlayerInput(this, tile?.energyConfig, allowPull = true)
val profiledEnergy = ProfiledLevelGaugeWidget(this, tile?.energy, energyWidget)
val balanceInputs = BooleanInputWithFeedback(this)
init {
if (tile != null) {
balanceInputs.with(tile::balanceInputs)
}
}
init {
addStorageSlot(inputSlots)
addStorageSlot(outputSlots)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB