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.pull", "Pull")
gui("side_mode.push", "Push") gui("side_mode.push", "Push")
gui("balance_inputs", "Balance input slots")
gui("sorting.default", "Default sorting") gui("sorting.default", "Default sorting")
gui("sorting.name", "Sort by name") gui("sorting.name", "Sort by name")
gui("sorting.id", "Sort by ID") gui("sorting.id", "Sort by ID")

View File

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

View File

@ -7,13 +7,10 @@ import net.minecraft.data.recipes.FinishedRecipe
import net.minecraft.resources.ResourceLocation import net.minecraft.resources.ResourceLocation
import net.minecraft.util.valueproviders.ConstantFloat import net.minecraft.util.valueproviders.ConstantFloat
import net.minecraft.util.valueproviders.FloatProvider import net.minecraft.util.valueproviders.FloatProvider
import net.minecraft.util.valueproviders.UniformFloat
import net.minecraft.world.item.crafting.RecipeSerializer import net.minecraft.world.item.crafting.RecipeSerializer
import ru.dbotthepony.mc.otm.recipe.PlatePressRecipe 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.set
import ru.dbotthepony.mc.otm.core.toJsonStrict import ru.dbotthepony.mc.otm.core.toJsonStrict
import ru.dbotthepony.mc.otm.data.getOrNull
class PlatePressFinishedRecipe(private val recipe: PlatePressRecipe) : FinishedRecipe { class PlatePressFinishedRecipe(private val recipe: PlatePressRecipe) : FinishedRecipe {
override fun serializeRecipeData(it: JsonObject) { override fun serializeRecipeData(it: JsonObject) {
@ -33,7 +30,7 @@ class PlatePressFinishedRecipe(private val recipe: PlatePressRecipe) : FinishedR
} }
override fun getType(): RecipeSerializer<*> { override fun getType(): RecipeSerializer<*> {
return PlatePressRecipeFactory return PlatePressRecipe.Companion
} }
override fun serializeAdvancement(): JsonObject? { override fun serializeAdvancement(): JsonObject? {
@ -74,7 +71,7 @@ class PlatePressShallowFinishedRecipe(
} }
override fun getType(): RecipeSerializer<*> { override fun getType(): RecipeSerializer<*> {
return PlatePressRecipeFactory return PlatePressRecipe.Companion
} }
override fun serializeAdvancement(): JsonObject? { 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 open fun jobUpdated(new: JobType?, old: JobType?, id: Int) {}
protected abstract fun onJobFinish(job: JobType, id: Int): JobStatus protected abstract fun onJobFinish(job: JobType, id: Int): JobStatus
protected abstract fun computeNextJob(id: Int): JobContainer<JobType> 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.config.MachinesConfig
import ru.dbotthepony.mc.otm.container.MatteryContainer import ru.dbotthepony.mc.otm.container.MatteryContainer
import ru.dbotthepony.mc.otm.container.HandlerFilter 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.core.math.Decimal
import ru.dbotthepony.mc.otm.menu.tech.PlatePressMenu import ru.dbotthepony.mc.otm.menu.tech.PlatePressMenu
import ru.dbotthepony.mc.otm.menu.tech.TwinPlatePressMenu 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))) 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 { companion object {
private val BASELINE_CONSUMPTION = Decimal(15) private val BASELINE_CONSUMPTION = Decimal(15)
} }

View File

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

View File

@ -274,11 +274,13 @@ class DeviceControls<out S : MatteryScreen<*>>(
val itemConfig: ItemConfigPlayerInput? = null, val itemConfig: ItemConfigPlayerInput? = null,
val energyConfig: EnergyConfigPlayerInput? = null, val energyConfig: EnergyConfigPlayerInput? = null,
val fluidConfig: FluidConfigPlayerInput? = null, val fluidConfig: FluidConfigPlayerInput? = null,
val balanceInputs: BooleanInputWithFeedback? = null,
) : EditablePanel<S>(screen, parent, x = parent.width + 3f, height = 0f, width = 0f) { ) : EditablePanel<S>(screen, parent, x = parent.width + 3f, height = 0f, width = 0f) {
val itemConfigButton: LargeRectangleButtonPanel<S>? val itemConfigButton: LargeRectangleButtonPanel<S>?
val energyConfigButton: LargeRectangleButtonPanel<S>? val energyConfigButton: LargeRectangleButtonPanel<S>?
val fluidConfigButton: LargeRectangleButtonPanel<S>? val fluidConfigButton: LargeRectangleButtonPanel<S>?
val redstoneControlsButton: LargeEnumRectangleButtonPanel<S, RedstoneSetting>? val redstoneControlsButton: LargeEnumRectangleButtonPanel<S, RedstoneSetting>?
val balanceInputsButton: LargeBooleanRectangleButtonPanel<S>?
private var nextY = 0f private var nextY = 0f
fun <P : EditablePanel<@UnsafeVariance S>> addButton(button: P): P { fun <P : EditablePanel<@UnsafeVariance S>> addButton(button: P): P {
@ -302,6 +304,18 @@ class DeviceControls<out S : MatteryScreen<*>>(
redstoneControlsButton = null 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) { if (itemConfig != null) {
itemConfigButton = addButton(object : LargeRectangleButtonPanel<S>(screen, this@DeviceControls, skinElement = Widgets18.ITEMS_CONFIGURATION) { itemConfigButton = addButton(object : LargeRectangleButtonPanel<S>(screen, this@DeviceControls, skinElement = Widgets18.ITEMS_CONFIGURATION) {
init { init {
@ -375,6 +389,9 @@ fun <S : MatteryScreen<*>> makeDeviceControls(
itemConfig: ItemConfigPlayerInput? = null, itemConfig: ItemConfigPlayerInput? = null,
energyConfig: EnergyConfigPlayerInput? = null, energyConfig: EnergyConfigPlayerInput? = null,
fluidConfig: FluidConfigPlayerInput? = null, fluidConfig: FluidConfigPlayerInput? = null,
balanceInputs: BooleanInputWithFeedback? = null,
): DeviceControls<S> { ): 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) ProgressGaugePanel(this, frame, menu.progressGauge1, 78f, PROGRESS_ARROW_TOP + 10f)
SlotPanel(this, frame, menu.outputSlots[1], 104f, PROGRESS_SLOT_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 return frame
} }

View File

@ -1,11 +1,19 @@
package ru.dbotthepony.mc.otm.container 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.Container
import net.minecraft.world.item.ItemStack import net.minecraft.world.item.ItemStack
import net.minecraftforge.common.capabilities.Capability import net.minecraftforge.common.capabilities.Capability
import net.minecraftforge.fluids.capability.IFluidHandler 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.iterator
import ru.dbotthepony.mc.otm.core.collect.nonEmpty 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.set(index: Int, value: ItemStack) = setItem(index, value)
operator fun Container.get(index: Int): ItemStack = getItem(index) operator fun Container.get(index: Int): ItemStack = getItem(index)
@ -109,3 +117,133 @@ inline fun Container.forEachNonEmpty(lambda: (ItemStack) -> Unit) {
lambda(value) 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>() { class BooleanInputWithFeedback(menu: MatteryMenu) : AbstractPlayerInputWithFeedback<Boolean>() {
override val input = menu.booleanInput { consumer?.invoke(it) } 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) { constructor(menu: MatteryMenu, state: KMutableProperty0<Boolean>) : this(menu) {
with(state) 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.MachineOutputSlot
import ru.dbotthepony.mc.otm.menu.MatteryPoweredMenu import ru.dbotthepony.mc.otm.menu.MatteryPoweredMenu
import ru.dbotthepony.mc.otm.menu.MatterySlot 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.EnergyConfigPlayerInput
import ru.dbotthepony.mc.otm.menu.input.ItemConfigPlayerInput import ru.dbotthepony.mc.otm.menu.input.ItemConfigPlayerInput
import ru.dbotthepony.mc.otm.menu.makeSlots import ru.dbotthepony.mc.otm.menu.makeSlots
@ -28,6 +29,14 @@ class TwinPlatePressMenu @JvmOverloads constructor(
val energyConfig = EnergyConfigPlayerInput(this, tile?.energyConfig, allowPull = true) val energyConfig = EnergyConfigPlayerInput(this, tile?.energyConfig, allowPull = true)
val profiledEnergy = ProfiledLevelGaugeWidget(this, tile?.energy, energyWidget) val profiledEnergy = ProfiledLevelGaugeWidget(this, tile?.energy, energyWidget)
val balanceInputs = BooleanInputWithFeedback(this)
init {
if (tile != null) {
balanceInputs.with(tile::balanceInputs)
}
}
init { init {
addStorageSlot(inputSlots) addStorageSlot(inputSlots)
addStorageSlot(outputSlots) addStorageSlot(outputSlots)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB