Skip to content

Commit

Permalink
add new api
Browse files Browse the repository at this point in the history
  • Loading branch information
SchnTgaiSpock committed Nov 4, 2024
1 parent 0a7fea8 commit 2ecfa47
Show file tree
Hide file tree
Showing 40 changed files with 3,960 additions and 33 deletions.
204 changes: 204 additions & 0 deletions docs/adr/0002-recipe-rewrite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# 2. Recipe rewrite

Date: 2024-11-03
Last update: 2024-11-03

**DO NOT rely on any APIs introduced until we finish the work completely!**

## Status

Phase 1: Work in progress

## Context

Slimefun currently lacks a robust recipe system. Multiblock crafting
does one thing, while electric machines do another, even though some
of them craft the same items.

Slimefun also lacks certain features that vanilla minecraft has, like true
shaped and shapeless recipes, tagged inputs, and the ability to edit recipes
without any code.

## Goals

The goal of this rewrite is to introduce an improved recipe system to
Slimefun, focusing on

- Ease of use: The API should be clean and the system intuitive for
developers to use
- Extensibility: Addons should be able to create and use their own types
of recipes with this system.
- Customizability: Server owners should be able to customize any and all
Slimefun recipes
- Performance: Should not blow up any servers

The new system should also be completely backwards compatible with the old.

## API Changes

### 5 main recipe classes

All recipes are now `Recipe` objects. It is an association between
inputs (see `RecipeInput`) and outputs (see `RecipeOutput`), along with other metadata
for how the recipe should be crafted -- recipe type, energy cost, base crafting duration, etc.

`RecipeInput`s are a list of `RecipeInputItem`s plus a `MatchProcedure` -- how the inputs of
the recipe should be matched to items in a multiblock/machine when crafting. The base ones are:

- Shaped/Shapeless: Exactly the same as vanilla
- Subset: How the current smeltery, etc. craft
- Shaped-flippable: The recipe can be flipped on the Y-axis
- Shaped-rotatable: The recipe can be rotated (currently only 45deg, 3x3)

`RecipeInputItem`s describe a single slot of a recipe and determines what
items match it. There can be a single item that matches (see `RecipeInputSlimefunItem`,
`RecipeInputItemStack`), or a list (tag) of items all of which can be used
in that slot (see `RecipeInputGroup`, `RecipeInputTag`).

`RecipeOutput`s are just a list of `RecipeOutputItem`s, all of which are crafted by the recipe.

An `RecipeOutputItem`s controls how an output is generated when the recipe is
crafted. It can be a single item (see `RecipeOutputItemStack`, `RecipeOutputSlimefunItem`),
or a group of items each with a certain weight of being output (see `RecipeOutputGroup`).

#### Examples

Here are the inputs and outputs of the recipe for a vanilla torch

```txt
RecipeInput (
{
EMPTY, EMPTY, EMPTY
EMPTY, RecipeInputGroup(COAL, CHARCOAL), EMPTY,
EMPTY, RecipeInputItemStack(STICK), EMPTY
},
SHAPED
)
RecipeOutput (
RecipeOutputItemStack(4 x TORCH)
)
```

Here are the inputs and outputs of a gold pan

```txt
RecipeInput (
{ RecipeOutputItemStack(GRAVEL) },
SUBSET
)
RecipeOutput (
RecipeOutputGroup(
40 RecipeOutputItemStack(FLINT)
5 RecipeOutputItemStack(IRON_NUGGET)
20 RecipeOutputItemStack(CLAY_BALL)
35 RecipeOutputSlimefunItem(SIFTED_ORE)
)
)
```

This would remove the need to use ItemSettings to determine the gold pan weights

### RecipeService

This is the public interface for the recipe system, there are methods here to add,
load, save, and search recipes. It also stores a map of `MatchProcedures` and
`RecipeType` by key for conversions from a string

## JSON Serialization

All recipes should be able to be serialized to and deserialized
from JSON. The schemas are shown below.

Here, `key` is the string representation of a namespaced key

`Recipe`

```txt
{
"input"?: RecipeInput
"output"?: RecipeOutput
"type": key | key[]
"energy"?: int
"crafting-time"?: int
"permission-node"?: string | string[]
}
```

The recipe deserializer also needs a `__filename` field, which is inserted when the file is read, so it doesn't (and shouldn't) be in the schema

`RecipeInput`

```txt
{
"items": string | string[]
"key": {
[key: string]: RecipeInputItem
}
"match"?: key
}
```

`RecipeOutput`

```txt
{
"items": RecipeOutputItem[]
}
```

`RecipeInputItem`*

```txt
{
"id": key
"amount"?: int
"durability"?: int
} | {
"tag": key
"amount"?: int
"durability"?: int
} | {
"group": RecipeInputItem[]
}
```

`RecipeOutputItem`*

```txt
{
"id": key
"amount"?: int
} | {
"group": RecipeInputItem[]
"weights"?: int[]
}
```

*In addition to those schemata, items can be in short form:

- Single items: `<namespace>:<id>|<amount>`
- Tags: `#<namespace>:<id>|<amount>`

## Extensibility

The 5 main recipe classes are all polymorphic, and subclasses can be used in their
stead, and should not affect the recipe system (as long as the right methods are
override, see javadocs)

### Custom serialization/deserialization

The default deserializers recognize subclasses with custom deserializers by
the presence of a `class` field in the json, which is the key of a
custom deserializer registered with Slimefun's `RecipeService`.
For custom serializers, override the `serialize` method on the subclass,
and ensure they also add the `class` field

## Phases

Each phase should be a separate PR

- Phase 1 - Add the new API
- Phase 2 - Migrate Slimefun toward the new API

The entire process should be seamless for the end users, and
backwards compatible with addons that haven't yet migrated
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.github.thebusybiscuit.slimefun4.api.recipes;

import java.util.List;
import java.util.Optional;

import javax.annotation.Nonnull;

import org.bukkit.inventory.ItemStack;

import com.google.gson.JsonElement;
import com.google.gson.JsonSerializationContext;

import io.github.thebusybiscuit.slimefun4.api.recipes.items.AbstractRecipeInputItem;
import io.github.thebusybiscuit.slimefun4.api.recipes.matching.InputMatchResult;
import io.github.thebusybiscuit.slimefun4.api.recipes.matching.MatchProcedure;
import io.github.thebusybiscuit.slimefun4.utils.RecipeUtils.BoundingBox;

public abstract class AbstractRecipeInput {

public abstract int getWidth();
public abstract void setWidth(int width);

public abstract int getHeight();
public abstract void setHeight(int height);

public abstract boolean isEmpty();

@Nonnull
public abstract List<AbstractRecipeInputItem> getItems();
public AbstractRecipeInputItem getItem(int index) {
return getItems().get(index);
}
public abstract void setItems(@Nonnull List<AbstractRecipeInputItem> items);

@Nonnull
public abstract MatchProcedure getMatchProcedure();
public abstract void setMatchProcedure(MatchProcedure matchProcedure);

public abstract Optional<BoundingBox> getBoundingBox();

public InputMatchResult match(List<ItemStack> givenItems) {
return getMatchProcedure().match(this, givenItems);
}

@Override
public abstract String toString();

public abstract JsonElement serialize(JsonSerializationContext context);

@Override
public abstract boolean equals(Object obj);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package io.github.thebusybiscuit.slimefun4.api.recipes;

import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.annotation.Nonnull;
import javax.annotation.ParametersAreNonnullByDefault;

import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;

import com.google.gson.JsonElement;
import com.google.gson.JsonSerializationContext;

import io.github.thebusybiscuit.slimefun4.api.recipes.matching.RecipeMatchResult;

public abstract class AbstractRecipeOutput {

// TODO find a better name
public static class Inserter {
private final Inventory inv;
private final Map<Integer, Integer> addToStacks;
private final Map<Integer, ItemStack> newStacks;
private final List<ItemStack> leftovers;

public Inserter(Inventory inv, Map<Integer, Integer> addToStacks, Map<Integer, ItemStack> newStacks, List<ItemStack> leftovers) {
this.inv = inv;
this.addToStacks = addToStacks;
this.newStacks = newStacks;
this.leftovers = leftovers;
}

public Inserter(Inventory inv) {
this.inv = inv;
this.addToStacks = Collections.emptyMap();
this.newStacks = Collections.emptyMap();
this.leftovers = Collections.emptyList();
}

public void insert() {
for (Map.Entry<Integer, Integer> entry : addToStacks.entrySet()) {
ItemStack item = inv.getItem(entry.getKey());
item.setAmount(item.getAmount() + entry.getValue());
}
for (Map.Entry<Integer, ItemStack> entry : newStacks.entrySet()) {
inv.setItem(entry.getKey(), entry.getValue());
}
}

public List<ItemStack> getLeftovers() {
return Collections.unmodifiableList(leftovers);
}
}

@ParametersAreNonnullByDefault
public abstract Inserter checkSpace(RecipeMatchResult matchResult, Inventory inventory, int[] outputSlots);

@Nonnull
@ParametersAreNonnullByDefault
public List<ItemStack> insertIntoInventory(RecipeMatchResult matchResult, Inventory inventory, int[] outputSlots) {
Inserter inserter = checkSpace(matchResult, inventory, outputSlots);
inserter.insert();
return inserter.getLeftovers();
}

@Nonnull
public abstract List<ItemStack> generateOutput(@Nonnull RecipeMatchResult result);

public abstract boolean isEmpty();

@Override
public abstract String toString();

@Override
public abstract boolean equals(Object obj);

public abstract JsonElement serialize(JsonSerializationContext context);

}
Loading

0 comments on commit 2ecfa47

Please sign in to comment.