Skip to content

Add Brew Packer factory to make BreweryX brews stackable - #960

Draft
xFier wants to merge 3 commits into
CivMC:mainfrom
xFier:factorymod-brew-packer
Draft

Add Brew Packer factory to make BreweryX brews stackable#960
xFier wants to merge 3 commits into
CivMC:mainfrom
xFier:factorymod-brew-packer

Conversation

@xFier

@xFier xFier commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

What

Adds a BREW_STACK recipe type to FactoryMod and a Brew Packer factory that makes BreweryX brews stackable.

Brews are potions, which vanilla caps at a stack size of one, so every bottle takes a whole inventory slot and bulk trade and transport are painful. The recipe hands back the same brew with the vanilla max_stack_size component set on it, so bottles merge into a single slot while staying real, drinkable, BreweryX readable brews. Stacks split and merge like any other item.

How it works

  • Sealing is required. BreweryX writes a fresh random scramble id into an unsealed brew on every save, so no two unsealed brews are ever equal and they could never stack. Pre-sealed brews are accepted as they are; unsealed ones are sealed during the run when seal_unsealed is on. Sealing is irreversible and rounds an odd quality down to the next even one, exactly as BreweryX's own sealing table does.
  • Sealed brews are inert. Brew#age, Brew#distillSlot and Brew#canDistill all short circuit on immutable, and BSealer / MCBarrel guard on isStripped / isStatic, so a stack cannot be aged, distilled or re-sealed in bulk. No duplication vector through BreweryX's mechanics.
  • Allow list only, keyed by BreweryX brew id (the yaml key in BreweryX's config) rather than display name. The id is stable across renames and unique by construction, whereas the name is a bad/normal/good triplet that is easy to pick the wrong part of. Ids cannot be checked at parse time because BreweryX loads POSTWORLD, so unknown ones are reported once on first use rather than failing silently.
  • Per brew stack sizes, so spirits can stack lower than everyday brews.
  • Already packed brews are skipped, so feeding one back in never charges the cost twice.

Drinking from a stack

This needed a fix. BreweryX swaps the potion for a plain one in PlayerItemConsumeEvent so vanilla does not apply its effects, which sends the server down the branch that finishes using the replacement rather than the held stack and then calls setItemInHand, overwriting the stack wholesale. Without this, drinking one brew off a stack of sixteen would leave the player holding a single glass bottle and destroy the other fifteen.

BrewStackListener sets the event's replacement to the rest of the stack and hands the empty bottle over separately, since overriding the replacement drops the one the server made. Stacks now drain a bottle at a time, the way eating from a stack of food does. It runs after BreweryX's handler (HIGHEST) so a drink refused in creative or by BPlayer#drink is left alone.

Config

BREW_STACK is documented in the plugin's default config.yml. The live factory is added to both the paper and zorweth FactoryMod configs:

  pack_brews:
    production_time: 2s
    name: Pack Brews
    type: BREW_STACK
    input:
      glass_bottle: { type: GLASS_BOTTLE, amount: 1 }
    max_stack_size: 16
    seal_unsealed: true
    packed_lore: Packed Brew ({size})
    allowed_brews:
      wheatbeer: 16
      beer: 16
      darkbeer: 16
      mead: 8
      whiskey: 8
      vodka: 4

allowed_brews also accepts a plain list of ids, and {size} in packed_lore is replaced with that brew's limit.

BreweryX is a compileOnly dependency on the jar already vendored in ansible/src/paper-plugins, matching zorweth, kitpvp and simpleadminhacks, and is a softdepend — without it the recipe is skipped with a warning and the listener is not registered.

Testing

This has not been run on a server yet. It compiles and the behaviour was traced through Paper, CraftBukkit and BreweryX source, but FactoryMod has no test suite so there is no runtime evidence. Reviewers should treat the following as unverified:

  • Factory formation, since setup cost uses containedExactlyIn and a wrong amount just makes the crafting table silently do nothing
  • Drinking from a stack: stack drops by exactly one, one glass bottle arrives, drunkenness applies once, no hand-slot desync; plus creative mode and the last bottle off a stack
  • Packing from both sealed and unsealed input, and that output merges to the configured size
  • Allow list rejection, repack skipping, per brew sizes, full output chest, and the unknown-id warning firing once

Balance note

Setup cost and the allow list are a starting point, not settled balance. Both are effectively permanent once players hold stock: the max_stack_size component stays on the item, and brews packed at different sizes or under different packed_lore will not stack with each other, so these are best fixed before release.

🤖 Generated with Claude Code

xFier and others added 3 commits August 5, 2026 15:25
Brews are potions, which vanilla caps at a stack size of one, so every
bottle takes a whole inventory slot and bulk trade is painful. The new
recipe hands back the same brew with the vanilla max stack size
component set on it, so bottles merge into one slot while staying real,
drinkable, BreweryX readable brews.

A brew has to be sealed first, because BreweryX writes a fresh random
scramble id into an unsealed brew on every save and no two of those are
ever equal. Pre-sealed brews are taken as they are, unsealed ones are
sealed during the run when seal_unsealed is on. Sealed brews are also
immutable, so they stay inert in barrels and distillers.

Only brews on the recipe's allow list are touched, each with its own
stack size. Entries are keyed by BreweryX brew id rather than display
name, since the id is stable across renames and unique, whereas the
name is a bad/normal/good triplet that is easy to pick wrong. Ids
cannot be validated at parse time because BreweryX loads POSTWORLD, so
unknown ones are reported once on first use.

BreweryX is a compileOnly dependency on the jar already vendored in
ansible/src/paper-plugins, matching zorweth, kitpvp and simpleadminhacks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BreweryX swaps the potion for a plain one in PlayerItemConsumeEvent so
vanilla does not apply its effects. That makes the server finish using
the replacement rather than the held stack, and put the result in the
hand slot with setItemInHand, which overwrites the stack wholesale.
Drinking one brew off a stack of sixteen would leave the player holding
a single glass bottle, destroying the other fifteen.

Setting the event's replacement decides what ends up in the hand, so
the rest of the stack goes back there and the empty bottle is handed
over separately, since overriding the replacement drops the one the
server made. Stacks now drain a bottle at a time, the way eating from a
stack of food does.

Runs after BreweryX's own handler, which sits on HIGHEST, so a drink it
refused in creative or through BPlayer#drink is left alone rather than
paying out a free bottle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A standard FCC factory carrying pack_brews and repair_brew_packer. The
allow list starts with the everyday brews at 16 and the spirits lower,
and packed brews are lored 'Packed Brew ({size})' so players can tell
them from ordinary ones and read the limit off the bottle.

Setup cost and the allow list are a starting point for balance rather
than a settled one. Note that both are effectively permanent once
players hold stock: the max stack size component stays on the item, and
brews packed at different sizes or under different lore will not stack
with each other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-project-automation github-project-automation Bot moved this to Backlog in CivMC Roadmap Aug 5, 2026
@xFier
xFier marked this pull request as draft August 5, 2026 14:27
@okx-code

okx-code commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

The LLM disclosure policy requires the model(s) used to be specified as well, can you add them?

@xFier

xFier commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Yes, I haven't fully reviewed all of this myself, nor tested it locally or a whole bunch of other things I need to do before this is marked ready for review. Just wanted to get it out of the context window before I leave for a few hours.

But, the model used was Claude Opus 5 with high effort.

brew_packer:
type: FCC
name: Brew Packer
citadelBreakReduction: 0.8

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistent with the compactor, but almost every other factory uses 1.0

type: FCC
name: Brew Packer
citadelBreakReduction: 0.8
setupcost:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A real balanced setup cost is going to be needed before this is released.

brew_packer:
type: FCC
name: Brew Packer
citadelBreakReduction: 0.8

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above comment.

type: DECOMPACT
input:
compact_lore: Compacted Item
pack_brews:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Essentially all of this needs to be balanced/reviewed further.

compileOnly(project(":plugins:heliodor-paper"))
compileOnly(project(":plugins:zorweth-paper"))
compileOnly(libs.worldedit)
compileOnly(files("../../ansible/src/paper-plugins/BreweryX-3.6.3.jar"))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure brewery has a maven repo entry or something we can use here instead. This seems pretty brittle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants