Skip to content

Commit

Permalink
Add support for an item or economy cost
Browse files Browse the repository at this point in the history
Add config option for default permission level
Update fallback head cache
Bump version
  • Loading branch information
PotatoPresident committed Feb 19, 2023
1 parent 0d91f93 commit f2dd287
Show file tree
Hide file tree
Showing 22 changed files with 221 additions and 40 deletions.
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# HeadIndex
Head Index is an easy to user server-side head database mod for the Fabric Loader. Head Index provides easy access to over 36,000 heads provided by minecraft-heads.com. Unlike a plugin, Head Index can also be used in singleplayer without the need for a server. New heads are added all the time and will be automatically added whenever you restart the server without needing to update. Heads all have a UUID and texture value set to eliminate the lagspikes experienced with traditional custom playerheads.
Head Index is an easy to user server-side head database mod for the Fabric Loader.
Head Index provides easy access to over 36,000 heads provided by minecraft-heads.com.
Unlike a plugin, Head Index can also be used in singleplayer without the need for a server.
New heads are added all the time and will be automatically added whenever you restart the server without needing to update.
Head Index also supports setting an item or economy (Common Economy API) cost for survival servers.


# Stuff
# Usage
### Commands

`/head` - Opens head GUI
Expand All @@ -11,7 +15,19 @@ Head Index is an easy to user server-side head database mod for the Fabric Loade

`/head search` <search> - Opens search menu

### Config
`config/head-index.json`
```json
{
"permissionLevel": 2, // The default permission level for the commands. Set to 0 to allow all players access
"economyType": "FREE", // The type of economy to use. Set to FREE to disable economy, ITEM to use an item, or ECONOMY to use an economy currency
"costType": "minecraft:diamond", // The identifier for the item or currency to use for the cost
"costAmount": 1 // The amount of the item or currency to use for the cost
}
```

### Permissions - Compatible with LuckPerms, PlayerRoles or any other farbic permission manager
You can adjust the default permission level in the config file to allow all players access without a permission manager.

`headindex.menu` - Grants /head and /head menu - Defaults to level 2 OP

Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies {
modImplementation include("me.lucko:fabric-permissions-api:0.1-SNAPSHOT")
modImplementation include("eu.pb4:sgui:1.2.0+1.19.3")
modImplementation include("fr.catcore:server-translations-api:1.4.19+1.19.3")
modImplementation include("eu.pb4:common-economy-api:1.0.0")
}

processResources {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ minecraft_version=1.19.3
yarn_mappings=1.19.3+build.3
loader_version=0.14.11
# Mod Properties
mod_version=1.0.10
mod_version=1.1.0
maven_group=us.potatoboy
archives_base_name=headindex
# Dependencies
Expand Down
48 changes: 42 additions & 6 deletions src/main/java/us/potatoboy/headindex/HeadIndex.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,66 @@

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import eu.pb4.common.economy.api.CommonEconomy;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
import net.fabricmc.fabric.api.transfer.v1.item.PlayerInventoryStorage;
import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.server.network.ServerPlayerEntity;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import us.potatoboy.headindex.api.Head;
import us.potatoboy.headindex.api.HeadDatabaseAPI;
import us.potatoboy.headindex.commands.HeadCommand;
import us.potatoboy.headindex.config.HeadIndexConfig;

import java.io.File;
import java.util.Comparator;
import java.util.concurrent.CompletableFuture;

public class HeadIndex implements ModInitializer {
public static final String MOD_ID = "headindex";
public static final Logger LOGGER = LogManager.getLogger();
public static HeadIndexConfig config;
public static final HeadDatabaseAPI HEAD_DATABASE = new HeadDatabaseAPI();
public static Multimap<Head.Category, Head> heads = HashMultimap.create();

@Override
public void onInitialize() {
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
new HeadCommand(dispatcher);
});
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> new HeadCommand(dispatcher));

CompletableFuture.runAsync(() -> {
heads = HEAD_DATABASE.getHeads();
});
CompletableFuture.runAsync(() -> heads = HEAD_DATABASE.getHeads());

config = HeadIndexConfig.loadConfig(new File(FabricLoader.getInstance().getConfigDir() + "/head-index.json"));
}

@SuppressWarnings("UnstableApiUsage")
public static void tryPurchase(ServerPlayerEntity player, int amount, Runnable onPurchase) {
var trueAmount = amount * HeadIndex.config.costAmount;

switch (HeadIndex.config.economyType) {
case FREE -> onPurchase.run();
case ITEM -> {
try (Transaction transaction = Transaction.openOuter()) {
long extracted = PlayerInventoryStorage.of(player).extract(ItemVariant.of(HeadIndex.config.getCostItem()), trueAmount, transaction);
if (extracted == trueAmount) {
transaction.commit();
onPurchase.run();
}
}
}
case ECONOMY -> {
var account = CommonEconomy.getAccounts(player, HeadIndex.config.getCostCurrency(player.server)).stream().min(Comparator.comparing(x -> -x.balance())).orElse(null);

if (account != null) {
var transaction = account.decreaseBalance(trueAmount);
if (transaction.isSuccessful()) {
onPurchase.run();
}
}
}
}
}
}
2 changes: 1 addition & 1 deletion src/main/java/us/potatoboy/headindex/api/Head.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public String getTagsOrEmpty() {
public ItemStack createStack() {
ItemStack stack = new ItemStack(Items.PLAYER_HEAD);
if (name != null) {
stack.setCustomName(Text.literal(name));
stack.setCustomName(Text.literal(name).styled(style -> style.withItalic(false)));
}

if (tags != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
import me.lucko.fabric.api.permissions.v0.Permissions;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import us.potatoboy.headindex.HeadIndex;
import us.potatoboy.headindex.commands.subcommands.MenuCommand;
import us.potatoboy.headindex.commands.subcommands.SearchCommand;

public class HeadCommand {
public HeadCommand(CommandDispatcher<ServerCommandSource> dispatcher) {
LiteralCommandNode<ServerCommandSource> root = CommandManager
.literal("head")
.requires(Permissions.require("headindex.menu", 2))
.requires(Permissions.require("headindex.menu", HeadIndex.config.permissionLevel))
.executes(MenuCommand::openMenu)
.build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,19 @@
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import us.potatoboy.headindex.BuildableCommand;
import us.potatoboy.headindex.HeadIndex;
import us.potatoboy.headindex.gui.HeadGui;

public class MenuCommand implements BuildableCommand {
@Override
public LiteralCommandNode<ServerCommandSource> build() {
return CommandManager.literal("menu")
.requires(Permissions.require("headindex.menu", 2))
.requires(Permissions.require("headindex.menu", HeadIndex.config.permissionLevel))
.executes(MenuCommand::openMenu)
.build();
}

public static int openMenu(CommandContext<ServerCommandSource> context) throws CommandSyntaxException {
public static int openMenu(CommandContext<ServerCommandSource> context) {
new HeadGui(context.getSource().getPlayer()).open();

return 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import us.potatoboy.headindex.BuildableCommand;
import us.potatoboy.headindex.HeadIndex;
import us.potatoboy.headindex.gui.HeadGui;

public class SearchCommand implements BuildableCommand {
@Override
public LiteralCommandNode<ServerCommandSource> build() {
return CommandManager.literal("search")
.requires(Permissions.require("headindex.search", 2))
.requires(Permissions.require("headindex.search", HeadIndex.config.permissionLevel))
.then(CommandManager.argument("term", StringArgumentType.word())
.executes(SearchCommand::openSearch))
.build();
}

public static int openSearch(CommandContext<ServerCommandSource> context) throws CommandSyntaxException {
public static int openSearch(CommandContext<ServerCommandSource> context) {
new HeadGui(context.getSource().getPlayer()).openSearch(StringArgumentType.getString(context, "term"));

return 1;
Expand Down
91 changes: 91 additions & 0 deletions src/main/java/us/potatoboy/headindex/config/HeadIndexConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package us.potatoboy.headindex.config;

import com.google.gson.*;
import eu.pb4.common.economy.api.CommonEconomy;
import eu.pb4.common.economy.api.EconomyCurrency;
import net.minecraft.item.Item;
import net.minecraft.registry.Registries;
import net.minecraft.server.MinecraftServer;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;

import java.io.*;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;

public class HeadIndexConfig {
private static final Gson GSON = new GsonBuilder().registerTypeAdapter(Identifier.class, new IdentifierSerializer()).setPrettyPrinting().create();

public enum EconomyType {
ITEM,
ECONOMY,
FREE
}

public int permissionLevel = 2;

public EconomyType economyType = EconomyType.FREE;
public Identifier costType = new Identifier("minecraft", "diamond");
public int costAmount = 1;

public Text getCost(MinecraftServer server) {
return switch (economyType) {
case ITEM -> Text.empty().append(getCostItem().getName()).append(Text.of(" × " + costAmount));
case ECONOMY -> getCostCurrency(server).formatValueText(costAmount, false);
case FREE -> Text.empty();
};
}

public Item getCostItem() {
return Registries.ITEM.get(costType);
}

public EconomyCurrency getCostCurrency(MinecraftServer server) {
return CommonEconomy.getCurrency(server, costType);
}

public static HeadIndexConfig loadConfig(File file) {
HeadIndexConfig config;

if (file.exists() && file.isFile()) {
try (
FileInputStream fileInputStream = new FileInputStream(file);
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader)
) {
config = GSON.fromJson(bufferedReader, HeadIndexConfig.class);
} catch (IOException e) {
throw new RuntimeException("Failed to load config", e);
}
} else {
config = new HeadIndexConfig();
}

config.saveConfig(file);

return config;
}

public void saveConfig(File config) {
try (
FileOutputStream stream = new FileOutputStream(config);
Writer writer = new OutputStreamWriter(stream, StandardCharsets.UTF_8)
) {
GSON.toJson(this, writer);
} catch (IOException e) {
throw new RuntimeException("Failed to load config", e);
}
}

public static class IdentifierSerializer implements JsonSerializer<Identifier>, JsonDeserializer<Identifier> {
@Override
public JsonElement serialize(Identifier src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toString());
}

@Override
public Identifier deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return Identifier.tryParse(json.getAsString());
}
}
}
20 changes: 19 additions & 1 deletion src/main/java/us/potatoboy/headindex/gui/HeadGui.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import net.minecraft.util.Formatting;
import us.potatoboy.headindex.HeadIndex;
import us.potatoboy.headindex.api.Head;
import us.potatoboy.headindex.config.HeadIndexConfig;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
Expand Down Expand Up @@ -175,8 +176,25 @@ public void onTick() {
texturesTag.add(textureValue);
propertiesTag.put("textures", texturesTag);
ownerTag.put("Properties", propertiesTag);

var builder = GuiElementBuilder.from(outputStack);
if (HeadIndex.config.economyType != HeadIndexConfig.EconomyType.FREE) {
builder.addLoreLine(Text.empty());
builder.addLoreLine(Text.translatable("text.headindex.price", HeadIndex.config.getCost(getPlayer().server)).styled(style -> style.withColor(Formatting.RED)));
}

this.setSlot(2, outputStack, (index, type, action, gui) -> player.currentScreenHandler.setCursorStack(outputStack.copy()));
this.setSlot(2, builder.asStack(), (index, type, action, gui) ->
HeadIndex.tryPurchase(player, 1, () -> {
var cursorStack = getPlayer().currentScreenHandler.getCursorStack();
if (player.currentScreenHandler.getCursorStack().isEmpty()) {
player.currentScreenHandler.setCursorStack(outputStack.copy());
} else if (outputStack.isItemEqual(cursorStack) && ItemStack.areNbtEqual(outputStack, cursorStack) && cursorStack.getCount() < cursorStack.getMaxCount()) {
cursorStack.increment(1);
} else {
player.dropItem(outputStack.copy(), false);
}
})
);
});
}
}
Expand Down
Loading

0 comments on commit f2dd287

Please sign in to comment.