This commit is contained in:
Bruno Rybársky 2024-08-19 19:51:50 +02:00
commit 346a14c2be
26 changed files with 1791 additions and 0 deletions

118
.gitignore vendored Normal file

@ -0,0 +1,118 @@
# User-specific stuff
.idea/
*.iml
*.ipr
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
.gradle
build/
# Ignore Gradle GUI config
gradle-app.setting
# Cache of project
.gradletasknamecache
**/build/
# Common working directory
run/
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar

21
LICENSE.txt Normal file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2024
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

111
build.gradle Normal file

@ -0,0 +1,111 @@
plugins {
id 'fabric-loom' version '1.7-SNAPSHOT'
id 'maven-publish'
}
version = project.mod_version
group = project.maven_group
base {
archivesName = project.archives_base_name
}
repositories {
// Add repositories to retrieve artifacts from in here.
// You should only use this when depending on other mods because
// Loom adds the essential maven repositories to download Minecraft and libraries from automatically.
// See https://docs.gradle.org/current/userguide/declaring_repositories.html
// for more information about repositories.
maven { url 'https://maven.nucleoid.xyz' }
maven {
url 'https://kneelawk.com/maven/'
name 'Kneelawk Maven'
}
maven {
// For LibNetworkStack, if using 'syncing' module
url 'https://maven.alexiil.uk/'
name 'AlexIIL Maven'
}
}
dependencies {
// To change the versions see the gradle.properties file
minecraft "com.mojang:minecraft:${project.minecraft_version}"
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
// Fabric API. This is technically optional, but you probably want it anyway.
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
include(modImplementation("eu.pb4:polymer-core:${project.polymer_version}"))
modImplementation include("eu.pb4:polymer-resource-pack:${project.polymer_version}")
modImplementation include("eu.pb4:polymer-autohost:${project.polymer_version}")
modImplementation include("eu.pb4:polymer-blocks:${project.polymer_version}")
modImplementation include("eu.pb4:polymer-virtual-entity:${project.polymer_version}")
include(modImplementation("xyz.nucleoid:server-translations-api:${project.server_translations_api_version}"))
modImplementation include("eu.pb4:sgui:${project.servergui_version}")
}
processResources {
inputs.property "version", project.version
inputs.property "minecraft_version", project.minecraft_version
inputs.property "loader_version", project.loader_version
filteringCharset "UTF-8"
filesMatching("fabric.mod.json") {
expand "version": project.version,
"minecraft_version": project.minecraft_version,
"loader_version": project.loader_version
}
}
def targetJavaVersion = 21
tasks.withType(JavaCompile).configureEach {
// ensure that the encoding is set to UTF-8, no matter what the system default is
// this fixes some edge cases with special characters not displaying correctly
// see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html
// If Javadoc is generated, this must be specified in that task too.
it.options.encoding = "UTF-8"
if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) {
it.options.release.set(targetJavaVersion)
}
}
java {
def javaVersion = JavaVersion.toVersion(targetJavaVersion)
if (JavaVersion.current() < javaVersion) {
toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
}
// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
// if it is present.
// If you remove this line, sources will not be generated.
withSourcesJar()
}
jar {
from("LICENSE") {
rename { "${it}_${project.archivesBaseName}" }
}
}
// configure the maven publication
publishing {
publications {
create("mavenJava", MavenPublication) {
artifactId = project.archives_base_name
from components.java
}
}
// See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing.
repositories {
// Add repositories to publish to here.
// Notice: This block does NOT have the same function as the block in the top level.
// The repositories here will be used for publishing your artifact, not for
// retrieving dependencies.
}
}

20
gradle.properties Normal file

@ -0,0 +1,20 @@
# Done to increase the memory available to gradle.
org.gradle.jvmargs=-Xmx1G
# Fabric Properties
# check these on https://modmuss50.me/fabric.html
minecraft_version=1.21.1
yarn_mappings=1.21.1+build.3
loader_version=0.16.2
# Mod Properties
mod_version=1.0
maven_group=systems.brn
archives_base_name=servershop
# Dependencies
# check this on https://modmuss50.me/fabric.html
fabric_version=0.102.1+1.21.1
# Dependencies
polymer_version=0.9.9+1.21
server_translations_api_version=2.3.1+1.21-pre2
servergui_version=1.6.0+1.21

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

249
gradlew vendored Executable file

@ -0,0 +1,249 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

92
gradlew.bat vendored Normal file

@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

9
settings.gradle Normal file

@ -0,0 +1,9 @@
pluginManagement {
repositories {
maven {
name = 'Fabric'
url = 'https://maven.fabricmc.net/'
}
gradlePluginPortal()
}
}

@ -0,0 +1,6 @@
package systems.brn.servershop;
public record ItemPrice(
int buyPrice, int sellPrice
) {
}

@ -0,0 +1,124 @@
package systems.brn.servershop;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.LongArgumentType;
import eu.pb4.polymer.resourcepack.api.PolymerResourcePackUtils;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.command.CommandRegistryAccess;
import net.minecraft.command.argument.EntityArgumentType;
import net.minecraft.command.argument.ItemStackArgumentType;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayNetworkHandler;
import net.minecraft.server.network.ServerPlayerEntity;
import systems.brn.servershop.commands.*;
import systems.brn.servershop.lib.BalanceManager;
import systems.brn.servershop.lib.PriceStorage;
import java.util.HashMap;
import static net.minecraft.server.command.CommandManager.argument;
import static net.minecraft.server.command.CommandManager.literal;
public class ServerShop implements ModInitializer {
public static PriceStorage priceStorage = null;
public static BalanceManager balanceManager = null;
public static final String MOD_ID = "servershop";
public static final HashMap<ServerPlayerEntity, Integer> balances = new HashMap<>();
@Override
public void onInitialize() {
ServerLifecycleEvents.SERVER_STARTED.register(this::onServerStarted);
ServerLifecycleEvents.SERVER_STOPPED.register(this::onServerStopped);
CommandRegistrationCallback.EVENT.register(this::commandRegister);
ServerPlayConnectionEvents.JOIN.register(this::onPlayerJoin);
PolymerResourcePackUtils.addModAssets(MOD_ID);
PolymerResourcePackUtils.markAsRequired();
}
private void onPlayerJoin(ServerPlayNetworkHandler serverPlayNetworkHandler, PacketSender packetSender, MinecraftServer server) {
if (packetSender instanceof ServerPlayerEntity player) {
balanceManager.onJoin(player);
}
}
private void commandRegister(CommandDispatcher<ServerCommandSource> dispatcher, CommandRegistryAccess commandRegistryAccess, CommandManager.RegistrationEnvironment registrationEnvironment) {
dispatcher.register(
literal("shop")
.executes(ShopCommand::run));
dispatcher.register(
literal("pay")
.then(argument("recipient", EntityArgumentType.player())
.then(argument("amount", IntegerArgumentType.integer(1)))
)
.executes(PayCommand::run)
);
dispatcher.register(
literal("balance")
.then(argument("recipient", EntityArgumentType.player()
)
.requires(serverCommandSource -> serverCommandSource.hasPermissionLevel(2))
.executes(BalanceCommand::others)
)
.then(literal("set")
.requires(serverCommandSource -> serverCommandSource.hasPermissionLevel(2))
.then(argument("balance", LongArgumentType.longArg(1))
.executes(BalanceCommand::selfSet)
.then(argument("recipient", EntityArgumentType.player())
.executes(BalanceCommand::othersSet))
)
)
.executes(BalanceCommand::self)
);
dispatcher.register(literal("shopprices")
.requires(serverCommandSource -> serverCommandSource.hasPermissionLevel(2))
.then(literal("load").executes(ShopPricesCommand::load))
.then(literal("save").executes(ShopPricesCommand::save))
.then(literal("reset").executes(ShopPricesCommand::reset))
.then(literal("set")
.then(argument("item", ItemStackArgumentType.itemStack(commandRegistryAccess))
.then(argument("buyprice", IntegerArgumentType.integer())
.then(argument("sellprice", IntegerArgumentType.integer())
.executes(ShopPricesCommand::set)
)
)
)
)
);
dispatcher.register(literal("buy")
.then(argument("item", ItemStackArgumentType.itemStack(commandRegistryAccess))
.then(argument("count", IntegerArgumentType.integer())
.executes(StoreCommands::buyCount))
.executes(StoreCommands::buyOne)
)
);
dispatcher.register(literal("sell")
.then(argument("item", ItemStackArgumentType.itemStack(commandRegistryAccess))
.then(argument("count", IntegerArgumentType.integer())
.executes(StoreCommands::sellCount))
.executes(StoreCommands::sellOne)
)
);
}
private void onServerStarted(MinecraftServer server) {
priceStorage = new PriceStorage(server);
balanceManager = new BalanceManager(server);
}
private void onServerStopped(MinecraftServer server) {
balanceManager.saveBalance();
}
}

@ -0,0 +1,70 @@
package systems.brn.servershop.commands;
import com.mojang.brigadier.arguments.LongArgumentType;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.command.argument.EntityArgumentType;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import static systems.brn.servershop.ServerShop.balanceManager;
public class BalanceCommand {
public static int others(CommandContext<ServerCommandSource> ctx) {
try {
ServerCommandSource playerObj = ctx.getSource();
ServerPlayerEntity player = ctx.getSource().getPlayer();
ServerPlayerEntity target = EntityArgumentType.getPlayer(ctx, "recipient");
if (player != null && target != null) {
long senderBalance = balanceManager.getBalance(target);
playerObj.sendFeedback(() -> Text.translatable("message.servershop.balance.other", target.getName(), senderBalance), false);
}
} catch (Exception ignored) {
return 1;
}
return 0;
}
public static int self(CommandContext<ServerCommandSource> ctx) {
try {
ServerCommandSource playerObj = ctx.getSource();
ServerPlayerEntity player = ctx.getSource().getPlayer();
if (player != null) {
long senderBalance = balanceManager.getBalance(player);
playerObj.sendFeedback(() -> Text.translatable("message.servershop.balance.self", senderBalance), false);
}
} catch (Exception ignored) {
return 1;
}
return 0;
}
public static int selfSet(CommandContext<ServerCommandSource> ctx) {
try {
ServerPlayerEntity player = ctx.getSource().getPlayer();
long senderBalance = LongArgumentType.getLong(ctx, "balance");
if (player != null) {
balanceManager.setBalance(player, senderBalance);
}
} catch (Exception ignored) {
return 1;
}
return 0;
}
public static int othersSet(CommandContext<ServerCommandSource> ctx) {
try {
ServerCommandSource playerObj = ctx.getSource();
ServerPlayerEntity player = ctx.getSource().getPlayer();
long senderBalance = LongArgumentType.getLong(ctx, "balance");
ServerPlayerEntity target = EntityArgumentType.getPlayer(ctx, "recipient");
if (player != null && target != null) {
balanceManager.setBalance(target, senderBalance);
playerObj.sendFeedback(() -> Text.translatable("message.servershop.balance.set_other", target.getName(), senderBalance), false);
}
} catch (Exception ignored) {
return 1;
}
return 0;
}
}

@ -0,0 +1,34 @@
package systems.brn.servershop.commands;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.command.argument.EntityArgumentType;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import static systems.brn.servershop.ServerShop.balanceManager;
public class PayCommand {
public static int run(CommandContext<ServerCommandSource> ctx) {
try {
ServerCommandSource senderObj = ctx.getSource();
ServerPlayerEntity sender = ctx.getSource().getPlayer();
ServerPlayerEntity target = EntityArgumentType.getPlayer(ctx, "recipient");
int amount = IntegerArgumentType.getInteger(ctx, "amount");
if (sender != null) {
long senderBalance = balanceManager.getBalance(sender);
if (senderBalance >= amount && amount > 0) {
balanceManager.removeBalance(sender, amount);
balanceManager.addBalance(target, amount);
senderObj.sendFeedback(() -> Text.translatable("message.servershop.pay.success", amount, target.getName(), senderBalance), false);
} else {
senderObj.sendFeedback(() -> Text.translatable("message.servershop.pay.bad_amount", amount, senderBalance), false);
}
}
return 0;
} catch (Exception ignored) {
return 1;
}
}
}

@ -0,0 +1,15 @@
package systems.brn.servershop.commands;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import systems.brn.servershop.screens.ShopScreen;
public class ShopCommand {
public static int run(CommandContext<ServerCommandSource> serverCommandSourceCommandContext) {
ServerPlayerEntity player = serverCommandSourceCommandContext.getSource().getPlayer();
ShopScreen shopScreen = new ShopScreen(player);
shopScreen.open();
return 0;
}
}

@ -0,0 +1,45 @@
package systems.brn.servershop.commands;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.command.argument.ItemStackArgumentType;
import net.minecraft.item.Item;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.Text;
import systems.brn.servershop.ServerShop;
public class ShopPricesCommand {
public static int load(CommandContext<ServerCommandSource> ctx) {
ServerShop.priceStorage.load();
ctx.getSource().sendFeedback(() -> Text.translatable("message.servershop.storage.load"), false);
return 1;
}
public static int save(CommandContext<ServerCommandSource> ctx) {
boolean success = ServerShop.priceStorage.save();
ctx.getSource().sendFeedback(() ->
Text.translatable(success ? "message.servershop.storage.save" : "message.servershop.storage.save_fail"), false);
return 1;
}
public static int reset(CommandContext<ServerCommandSource> ctx) {
ServerShop.priceStorage.generateEmpty();
ctx.getSource().sendFeedback(() -> Text.translatable("message.servershop.storage.reset"), false);
return 1;
}
public static int set(CommandContext<ServerCommandSource> ctx) {
Item item = ItemStackArgumentType.getItemStackArgument(ctx, "item").getItem();
int buyPrice = IntegerArgumentType.getInteger(ctx, "buyprice");
int sellPrice = IntegerArgumentType.getInteger(ctx, "sellprice");
String itemName = item.toString();
boolean success = ServerShop.priceStorage.setPrices(item, buyPrice, sellPrice);
if (success) {
ctx.getSource().sendFeedback(() -> Text.translatable("message.servershop.storage.set", itemName, buyPrice, sellPrice), false);
return 1;
} else {
ctx.getSource().sendFeedback(() -> Text.translatable("message.servershop.storage.set_fail", itemName), false);
return 0;
}
}
}

@ -0,0 +1,60 @@
package systems.brn.servershop.commands;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.command.argument.ItemStackArgumentType;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import static systems.brn.servershop.lib.ShopFunctions.buy;
import static systems.brn.servershop.lib.ShopFunctions.sell;
public class StoreCommands {
public static int buyCount(CommandContext<ServerCommandSource> ctx) {
Item item = ItemStackArgumentType.getItemStackArgument(ctx, "item").getItem();
int count = IntegerArgumentType.getInteger(ctx, "count");
ItemStack itemStack = new ItemStack(item, count);
ServerPlayerEntity player = ctx.getSource().getPlayer();
if (player != null) {
buy(itemStack, player);
return 0;
}
return 1;
}
public static int buyOne(CommandContext<ServerCommandSource> ctx) {
Item item = ItemStackArgumentType.getItemStackArgument(ctx, "item").getItem();
ItemStack itemStack = new ItemStack(item);
ServerPlayerEntity player = ctx.getSource().getPlayer();
if (player != null) {
buy(itemStack, player);
return 0;
}
return 1;
}
public static int sellCount(CommandContext<ServerCommandSource> ctx) {
Item item = ItemStackArgumentType.getItemStackArgument(ctx, "item").getItem();
int count = IntegerArgumentType.getInteger(ctx, "count");
ItemStack itemStack = new ItemStack(item, count);
ServerPlayerEntity player = ctx.getSource().getPlayer();
if (player != null) {
sell(itemStack, player);
return 0;
}
return 1;
}
public static int sellOne(CommandContext<ServerCommandSource> ctx) {
Item item = ItemStackArgumentType.getItemStackArgument(ctx, "item").getItem();
ItemStack itemStack = new ItemStack(item);
ServerPlayerEntity player = ctx.getSource().getPlayer();
if (player != null) {
sell(itemStack, player);
return 0;
}
return 1;
}
}

@ -0,0 +1,121 @@
package systems.brn.servershop.lib;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import net.minecraft.util.WorldSavePath;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Scanner;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock;
public class BalanceManager {
final private HashMap<UUID, Long> balances = new HashMap<>();
public final MinecraftServer server;
public final File balanceStorageFile;
private static final ReentrantLock lock = new ReentrantLock(); // Lock for in-memory operations
public BalanceManager(MinecraftServer server) {
this.server = server;
balanceStorageFile = server.getSavePath(WorldSavePath.ROOT).resolve("balances.csv").toFile();
loadBalance();
}
public long getBalance(UUID uuid) {
if (balances.containsKey(uuid)) {
return balances.get(uuid);
}
return 0L;
}
public long getBalance(ServerPlayerEntity player) {
return getBalance(player.getUuid());
}
public void addBalance(UUID uuid, long amount) {
balances.put(uuid, amount + getBalance(uuid));
saveBalance();
}
public void addBalance(ServerPlayerEntity player, long amount) {
addBalance(player.getUuid(), amount);
announceBalance(player, true);
}
public void removeBalance(UUID uuid, long amount) {
balances.put(uuid, getBalance(uuid) - amount);
saveBalance();
}
public void setBalance(UUID uuid, long amount) {
balances.put(uuid, getBalance(uuid) + amount);
}
public void setBalance(ServerPlayerEntity player, long amount) {
setBalance(player.getUuid(), amount);
announceBalance(player, true);
}
public void removeBalance(ServerPlayerEntity player, long amount) {
removeBalance(player.getUuid(), amount);
announceBalance(player, true);
}
public boolean saveBalance() {
lock.lock();
try {
FileWriter fileWriter = new FileWriter(balanceStorageFile, false);
for (UUID uuid : balances.keySet()) {
long balance = getBalance(uuid);
fileWriter.write(uuid.toString() + "," + balance + "\n");
}
fileWriter.close();
lock.unlock();
} catch (IOException e) {
lock.unlock();
return false;
}
return true;
}
public void onJoin(ServerPlayerEntity player) {
UUID uuid = player.getUuid();
if (!balances.containsKey(uuid)) {
balances.put(uuid, 0L);
}
}
public void loadBalance() {
lock.lock();
if (!balances.isEmpty()) {
balances.clear();
}
try {
Scanner scanner = new Scanner(balanceStorageFile);
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
String[] lineParts = line.split(",");
if (lineParts.length == 2) {
UUID uuid = UUID.fromString(lineParts[0]);
Long amount = Long.parseLong(lineParts[1]);
balances.put(uuid, amount);
}
}
} catch (FileNotFoundException ignored) {
saveBalance();
}
lock.unlock();
}
public void announceBalance(ServerPlayerEntity player, boolean overlay) {
UUID uuid = player.getUuid();
long balance = balances.getOrDefault(uuid, 0L);
player.sendMessage(Text.translatable("message.servershop.balance.self", balance), overlay);
}
}

@ -0,0 +1,202 @@
package systems.brn.servershop.lib;
import eu.pb4.sgui.api.elements.GuiElement;
import eu.pb4.sgui.api.elements.GuiElementBuilder;
import eu.pb4.sgui.api.elements.GuiElementBuilderInterface;
import eu.pb4.sgui.api.elements.GuiElementInterface;
import eu.pb4.sgui.api.gui.SimpleGui;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.screen.ScreenHandlerType;
import net.minecraft.screen.slot.Slot;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.sound.SoundCategory;
import net.minecraft.sound.SoundEvents;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
//from https://github.com/Patbox/get-off-my-lawn-reserved/blob/6c03c6b004f65616d1d8cb773c19da970eb8c1d1/src/main/java/draylar/goml/ui/PagedGui.java
@ApiStatus.Internal
public abstract class PagedGui extends SimpleGui {
public static final String GUI_PREVIOUS_PAGE = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMzEwODI5OGZmMmIyNjk1MWQ2ODNlNWFkZTQ2YTQyZTkwYzJmN2M3ZGQ0MWJhYTkwOGJjNTg1MmY4YzMyZTU4MyJ9fX0";
public static final String GUI_PREVIOUS_PAGE_BLOCKED = "ewogICJ0aW1lc3RhbXAiIDogMTY0MDYxNjE5MjE0MiwKICAicHJvZmlsZUlkIiA6ICJmMjc0YzRkNjI1MDQ0ZTQxOGVmYmYwNmM3NWIyMDIxMyIsCiAgInByb2ZpbGVOYW1lIiA6ICJIeXBpZ3NlbCIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS81MDgyMGY3NmUzZTA0MWM3NWY3NmQwZjMwMTIzMmJkZjQ4MzIxYjUzNGZlNmE4NTljY2I4NzNkMjk4MWE5NjIzIiwKICAgICAgIm1ldGFkYXRhIiA6IHsKICAgICAgICAibW9kZWwiIDogInNsaW0iCiAgICAgIH0KICAgIH0KICB9Cn0=";
public static final String GUI_NEXT_PAGE = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYzg2MTg1YjFkNTE5YWRlNTg1ZjE4NGMzNGYzZjNlMjBiYjY0MWRlYjg3OWU4MTM3OGU0ZWFmMjA5Mjg3In19fQ";
public static final String GUI_NEXT_PAGE_BLOCKED = "ewogICJ0aW1lc3RhbXAiIDogMTY0MDYxNjExMDQ4OCwKICAicHJvZmlsZUlkIiA6ICIxZjEyNTNhYTVkYTQ0ZjU5YWU1YWI1NmFhZjRlNTYxNyIsCiAgInByb2ZpbGVOYW1lIiA6ICJOb3RNaUt5IiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzdlNTc3MjBhNDg3OGM4YmNhYjBlOWM5YzQ3ZDllNTUxMjhjY2Q3N2JhMzQ0NWE1NGE5MWUzZTFlMWEyNzM1NmUiLAogICAgICAibWV0YWRhdGEiIDogewogICAgICAgICJtb2RlbCIgOiAic2xpbSIKICAgICAgfQogICAgfQogIH0KfQ==";
public static final String GUI_QUESTION_MARK = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYmM4ZWExZjUxZjI1M2ZmNTE0MmNhMTFhZTQ1MTkzYTRhZDhjM2FiNWU5YzZlZWM4YmE3YTRmY2I3YmFjNDAifX19";
public static final int PAGE_SIZE = 9 * 5;
protected final Runnable closeCallback;
public boolean ignoreCloseCallback;
public int page = 0;
public PagedGui(ServerPlayerEntity player, @Nullable Runnable closeCallback) {
super(ScreenHandlerType.GENERIC_9X6, player, false);
this.closeCallback = closeCallback;
}
@Override
public void onClose() {
if (this.closeCallback != null && !ignoreCloseCallback) {
this.closeCallback.run();
}
}
protected void nextPage() {
this.page = Math.min(this.getPageAmount() - 1, this.page + 1);
this.updateDisplay();
}
protected boolean canNextPage() {
return this.getPageAmount() > this.page + 1;
}
protected void previousPage() {
this.page = Math.max(0, this.page - 1);
this.updateDisplay();
}
protected boolean canPreviousPage() {
return this.page - 1 >= 0;
}
public void updateDisplay() {
var offset = page * PAGE_SIZE;
for (int i = 0; i < PAGE_SIZE; i++) {
var element = this.getElement(offset + i);
if (element == null) {
element = DisplayElement.empty();
}
if (element.element() != null) {
this.setSlot(i, element.element());
} else if (element.slot() != null) {
this.setSlotRedirect(i, element.slot());
}
}
for (int i = 0; i < 9; i++) {
var navElement = this.getNavElement(i);
if (navElement == null) {
navElement = DisplayElement.EMPTY;
}
if (navElement.element != null) {
this.setSlot(i + PAGE_SIZE, navElement.element);
} else if (navElement.slot != null) {
this.setSlotRedirect(i + PAGE_SIZE, navElement.slot);
}
}
}
protected abstract int getPageAmount();
protected abstract DisplayElement getElement(int id);
protected DisplayElement getNavElement(int id) {
return switch (id) {
case 0 -> DisplayElement.previousPage(this);
case 1 -> this.search();
case 7 -> DisplayElement.nextPage(this);
case 8 -> DisplayElement.of(
new GuiElementBuilder(Items.STRUCTURE_VOID)
.setName(Text.translatable(this.closeCallback != null ? "gui.back" : "mco.selectServer.close").formatted(Formatting.RED))
.hideDefaultTooltip().noDefaults()
.setCallback((i, clickType, slotActionType) -> {
playClickSound(this.player);
this.close();
})
);
default -> DisplayElement.filler();
};
}
protected DisplayElement search(){
return DisplayElement.filler();
}
public record DisplayElement(@Nullable GuiElementInterface element, @Nullable Slot slot) {
private static final DisplayElement EMPTY = DisplayElement.of(new GuiElement(ItemStack.EMPTY, GuiElementInterface.EMPTY_CALLBACK));
private static final DisplayElement FILLER = DisplayElement.of(
new GuiElementBuilder(Items.WHITE_STAINED_GLASS_PANE)
.setName(Text.empty())
.hideTooltip()
);
public static DisplayElement of(GuiElementInterface element) {
return new DisplayElement(element, null);
}
public static DisplayElement of(GuiElementBuilderInterface<?> element) {
return new DisplayElement(element.build(), null);
}
public static DisplayElement of(Slot slot) {
return new DisplayElement(null, slot);
}
public static DisplayElement nextPage(PagedGui gui) {
if (gui.canNextPage()) {
return DisplayElement.of(
new GuiElementBuilder(Items.PLAYER_HEAD)
.setName(Text.translatable("createWorld.customize.custom.next").formatted(Formatting.WHITE))
.hideDefaultTooltip().noDefaults()
.setSkullOwner(GUI_NEXT_PAGE)
.setCallback((x, y, z) -> {
playClickSound(gui.player);
gui.nextPage();
})
);
} else {
return DisplayElement.of(
new GuiElementBuilder(Items.PLAYER_HEAD)
.setName(Text.translatable("createWorld.customize.custom.next").formatted(Formatting.DARK_GRAY))
.hideDefaultTooltip().noDefaults()
.setSkullOwner(GUI_NEXT_PAGE_BLOCKED)
);
}
}
public static DisplayElement previousPage(PagedGui gui) {
if (gui.canPreviousPage()) {
return DisplayElement.of(
new GuiElementBuilder(Items.PLAYER_HEAD)
.setName(Text.translatable("createWorld.customize.custom.prev").formatted(Formatting.WHITE))
.hideDefaultTooltip().noDefaults()
.setSkullOwner(GUI_PREVIOUS_PAGE)
.setCallback((x, y, z) -> {
playClickSound(gui.player);
gui.previousPage();
})
);
} else {
return DisplayElement.of(
new GuiElementBuilder(Items.PLAYER_HEAD)
.setName(Text.translatable("createWorld.customize.custom.prev").formatted(Formatting.DARK_GRAY))
.hideDefaultTooltip().noDefaults()
.setSkullOwner(GUI_PREVIOUS_PAGE_BLOCKED)
);
}
}
public static DisplayElement filler() {
return FILLER;
}
public static DisplayElement empty() {
return EMPTY;
}
}
public static void playClickSound(ServerPlayerEntity player) {
player.playSoundToPlayer(SoundEvents.UI_BUTTON_CLICK.value(), SoundCategory.MASTER, 1, 1);
}
}

@ -0,0 +1,94 @@
package systems.brn.servershop.lib;
import net.minecraft.item.Item;
import net.minecraft.registry.Registries;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.Identifier;
import net.minecraft.util.WorldSavePath;
import systems.brn.servershop.ItemPrice;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Scanner;
public class PriceStorage {
public final MinecraftServer server;
public final File priceStorageFile;
public PriceStorage(MinecraftServer server) {
this.server = server;
priceStorageFile = server.getSavePath(WorldSavePath.ROOT).resolve("prices.csv").toFile();
load();
}
public final HashMap<Item, ItemPrice> prices = new HashMap<>();
public void generateEmpty() {
if (!prices.isEmpty()) {
prices.clear();
}
for (Item item : Registries.ITEM) {
ItemPrice itemPrice = new ItemPrice(0, 0);
prices.put(item, itemPrice);
}
}
public boolean setPrices(Item item, int buyPrice, int sellPrice) {
if (prices.containsKey(item)) {
prices.remove(item);
prices.put(item, new ItemPrice(buyPrice, sellPrice));
return true;
}
return false;
}
public void load() {
if (!prices.isEmpty()) {
prices.clear();
}
try {
Scanner scanner = new Scanner(priceStorageFile);
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
String[] lineParts = line.split(",");
if (lineParts.length == 3) {
String itemName = lineParts[0];
String[] itemIdentifierParts = itemName.split(":");
if (itemIdentifierParts.length == 2) {
Identifier itemIdentifier = Identifier.of(itemIdentifierParts[0], itemIdentifierParts[1]);
int buyPrice = Integer.parseInt(lineParts[1]);
int sellPrice = Integer.parseInt(lineParts[2]);
Item item = Registries.ITEM.get(itemIdentifier);
prices.put(item, new ItemPrice(buyPrice, sellPrice));
}
}
}
} catch (FileNotFoundException ignored) {
generateEmpty();
save();
}
}
public boolean save() {
try {
FileWriter fileWriter = new FileWriter(priceStorageFile, false);
for (Item item : prices.keySet()) {
ItemPrice itemPrice = prices.get(item);
String itemName = item.toString();
int buyPrice = itemPrice.buyPrice();
int sellPrice = itemPrice.sellPrice();
fileWriter.write(itemName + "," + buyPrice + "," + sellPrice + "\n");
}
fileWriter.close();
} catch (IOException e) {
return false;
}
return true;
}
}

@ -0,0 +1,59 @@
package systems.brn.servershop.lib;
import net.minecraft.command.argument.ItemStackArgumentType;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import systems.brn.servershop.ItemPrice;
import static systems.brn.servershop.ServerShop.balanceManager;
import static systems.brn.servershop.ServerShop.priceStorage;
import static systems.brn.servershop.lib.Util.*;
public class ShopFunctions {
public static void buy(ItemStack itemStack, ServerPlayerEntity player) {
Item item = itemStack.getItem();
PlayerInventory playerInventory = player.getInventory();
ItemPrice price = priceStorage.prices.getOrDefault(item, new ItemPrice(0, 0));
int buyPrice = price.buyPrice() * itemStack.getCount();
long playerBalance = balanceManager.getBalance(player);
if (buyPrice > 0) {
if (playerBalance >= buyPrice) {
if (canInsertItemIntoInventory(playerInventory, itemStack.copy()) >= itemStack.getCount()) {
ItemStack remaining = insertStackIntoInventory(playerInventory, itemStack.copy());
int toDeduce = buyPrice - (price.buyPrice() * remaining.getCount());
int boughtCount = itemStack.getCount() - remaining.getCount();
balanceManager.removeBalance(player, toDeduce);
player.sendMessage(Text.translatable("message.servershop.buy.success", boughtCount, itemStack.getName(), toDeduce), true);
} else {
player.sendMessage(Text.translatable("message.servershop.buy.inventory"), true);
}
} else {
player.sendMessage(Text.translatable("message.servershop.buy.not_enough", buyPrice, playerBalance, buyPrice - playerBalance), true);
}
} else {
player.sendMessage(Text.translatable("message.servershop.buy.not_available", itemStack.getName()), true);
}
}
public static void sell(ItemStack itemStack, ServerPlayerEntity player) {
Item item = itemStack.getItem();
PlayerInventory playerInventory = player.getInventory();
ItemPrice price = priceStorage.prices.getOrDefault(item, new ItemPrice(0, 0));
int sellPrice = price.sellPrice();
if (sellPrice > 0) {
int removed = removeFromInventory(playerInventory, itemStack.copy(), itemStack.getCount());
if (removed == 0) {
player.sendMessage(Text.translatable("message.servershop.sell.not_enough"), true);
}
int toAdd = sellPrice * (itemStack.getCount() - removed);
int soldCount = itemStack.getCount() - removed;
balanceManager.addBalance(player, toAdd);
player.sendMessage(Text.translatable("message.servershop.sell.success", soldCount, itemStack.getName(), toAdd), true);
} else {
player.sendMessage(Text.translatable("message.servershop.sell.not_available", itemStack.getName()), true);
}
}
}

@ -0,0 +1,102 @@
package systems.brn.servershop.lib;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.inventory.Inventory;
import net.minecraft.item.ItemStack;
import net.minecraft.util.Identifier;
import systems.brn.servershop.ServerShop;
public class Util {
public static Identifier id(String path) {
return Identifier.of(ServerShop.MOD_ID, path);
}
public static int canInsertItemIntoInventory(Inventory inventory, ItemStack itemStack) {
// Get the player's inventory
int maxInsert = 0;
if (inventory instanceof PlayerInventory playerInventory) {
// Iterate through the slots in the player's inventory
for (int i = 0; i < playerInventory.main.size(); i++) {
ItemStack slotStack = playerInventory.main.get(i);
maxInsert = canInsertToStack(slotStack, itemStack, maxInsert);
}
} else {
for (int i = 0; i < inventory.size(); i++) {
ItemStack slotStack = inventory.getStack(i);
maxInsert = canInsertToStack(slotStack, itemStack, maxInsert);
}
}
return maxInsert; // Return the maximum insertion count
}
public static int removeFromInventory(Inventory inventory, ItemStack stackToRemove, int remainingToRemove) {
for (int i = 0; i < inventory.size(); i++) {
ItemStack slotStack = inventory.getStack(i);
if (canCombine(slotStack, stackToRemove)) {
int removeAmount = Math.min(slotStack.getCount(), remainingToRemove);
slotStack.decrement(removeAmount);
remainingToRemove -= removeAmount;
inventory.markDirty();
if (slotStack.isEmpty()) {
inventory.setStack(i, ItemStack.EMPTY);
}
if (remainingToRemove <= 0) {
return remainingToRemove;
}
}
}
return remainingToRemove;
}
public static int canInsertToStack(ItemStack stack1, ItemStack stack2, int maxInsert) {
if (stack1.isEmpty() || ItemStack.areItemsEqual(stack1, stack2)) {
int remainingSpace = stack1.isEmpty() ? stack2.getMaxCount() : stack1.getMaxCount() - stack1.getCount();
maxInsert += remainingSpace;
// If the maximum insertion count is greater than or equal to the item count, return the item count
if (maxInsert >= stack2.getCount()) {
return stack2.getCount();
}
}
return maxInsert;
}
public static boolean canCombine(ItemStack stack1, ItemStack stack2) {
return !stack1.isEmpty() && stack1.getItem() == stack2.getItem() && ItemStack.areItemsAndComponentsEqual(stack1, stack2);
}
public static ItemStack insertStackIntoInventory(Inventory inventory, ItemStack stack) {
// First, try to merge with existing stacks
for (int i = 0; i < inventory.size(); i++) {
ItemStack slotStack = inventory.getStack(i);
if (canCombine(slotStack, stack)) {
int transferAmount = Math.min(stack.getCount(), slotStack.getMaxCount() - slotStack.getCount());
if (transferAmount > 0) {
slotStack.increment(transferAmount);
stack.decrement(transferAmount);
inventory.markDirty();
if (stack.isEmpty()) {
return ItemStack.EMPTY;
}
}
}
}
// Next, try to find an empty slot
for (int i = 0; i < inventory.size(); i++) {
ItemStack slotStack = inventory.getStack(i);
if (slotStack.isEmpty()) {
inventory.setStack(i, stack.copy());
stack.setCount(0);
inventory.markDirty();
return ItemStack.EMPTY;
}
}
return stack;
}
}

@ -0,0 +1,26 @@
package systems.brn.servershop.screens;
import eu.pb4.sgui.api.gui.AnvilInputGui;
import eu.pb4.sgui.api.gui.SimpleGui;
public class SearchScreen extends AnvilInputGui {
private final SimpleGui parentScreen;
public SearchScreen(SimpleGui parentScreen, String defaultText) {
super(parentScreen.getPlayer(), parentScreen.getLockPlayerInventory());
this.parentScreen = parentScreen;
parentScreen.close();
this.setDefaultInputValue(defaultText);
}
@Override
public void onClose() {
super.onClose();
parentScreen.open();
String query = this.getInput();
if (parentScreen instanceof ShopScreen shopScreen) {
shopScreen.doSearch(query);
}
}
}

@ -0,0 +1,144 @@
package systems.brn.servershop.screens;
import eu.pb4.sgui.api.ClickType;
import eu.pb4.sgui.api.elements.GuiElementBuilder;
import eu.pb4.sgui.api.elements.GuiElementInterface;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.LoreComponent;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.screen.slot.SlotActionType;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import systems.brn.servershop.ItemPrice;
import systems.brn.servershop.ServerShop;
import systems.brn.servershop.lib.PagedGui;
import systems.brn.servershop.lib.ShopFunctions;
import java.util.*;
import static systems.brn.servershop.ServerShop.balanceManager;
import static systems.brn.servershop.lib.ShopFunctions.buy;
import static systems.brn.servershop.lib.ShopFunctions.sell;
import static systems.brn.servershop.lib.Util.*;
public class ShopScreen extends PagedGui {
public String searchQuery = "";
final public HashMap<Item, ItemPrice> filteredPrices = new HashMap<>();
public ShopScreen(ServerPlayerEntity player) {
super(player, null);
setTitle(Text.translatable("gui.servershop.shop.title"));
}
@Override
public boolean open() {
updateDisplay();
return super.open();
}
@Override
public void updateDisplay() {
filterPrices();
super.updateDisplay();
}
public void filterPrices() {
filteredPrices.clear();
for (Item item : ServerShop.priceStorage.prices.keySet()) {
String itemName = item.toString();
if (itemName.contains(searchQuery)) {
ItemPrice price = ServerShop.priceStorage.prices.get(item);
if (price.buyPrice() > 0 || price.sellPrice() > 0) {
filteredPrices.put(item, price);
}
}
}
}
public void doSearch(String query) {
searchQuery = query;
updateDisplay();
}
@Override
protected int getPageAmount() {
return Math.ceilDivExact(filteredPrices.size(), PAGE_SIZE);
}
@Override
public boolean onClick(int index, ClickType type, SlotActionType action, GuiElementInterface element) {
Item item = element.getItemStack().getItem();
ItemStack itemStack = item.getDefaultStack();
if (type.shift) {
itemStack.setCount(itemStack.getMaxCount());
}
if (type.isLeft) { //buy
buy(itemStack, player);
} else if (type.isRight) { //sell
sell(itemStack, player);
}
return false;
}
public static ItemStack getItemStack(Map.Entry<Item, ItemPrice> price) {
ItemPrice itemPrice = price.getValue();
Item item = price.getKey();
int buyPrice = itemPrice.buyPrice();
int sellPrice = itemPrice.sellPrice();
ItemStack stack = item.getDefaultStack();
LoreComponent originalLore = stack.getOrDefault(DataComponentTypes.LORE, new LoreComponent(List.of()));
ArrayList<Text> lore = new ArrayList<>(originalLore.lines());
if (buyPrice > 0) {
lore.add(Text.translatable("gui.servershop.item.buyprice", buyPrice));
}
if (sellPrice > 0) {
lore.add(Text.translatable("gui.servershop.item.sellprice", sellPrice));
}
stack.set(DataComponentTypes.LORE, new LoreComponent(lore));
return stack;
}
@Override
protected DisplayElement getElement(int id) {
List<Map.Entry<Item, ItemPrice>> list = new ArrayList<>(filteredPrices.entrySet());
if (id < list.size()) {
Map.Entry<Item, ItemPrice> itemPriceEntry = list.get(id);
ItemStack stack = getItemStack(itemPriceEntry);
GuiElementBuilder elementBuilder = new GuiElementBuilder(stack);
return DisplayElement.of(elementBuilder);
}
return DisplayElement.filler();
}
@Override
protected DisplayElement search() {
if (searchQuery == null || searchQuery.isEmpty() || searchQuery.equals("*")) {
searchQuery = "Filter not set";
}
return DisplayElement.of(
new GuiElementBuilder(Items.PLAYER_HEAD)
.setName(Text.literal(searchQuery).formatted(Formatting.WHITE))
.hideDefaultTooltip().noDefaults()
.setSkullOwner(GUI_QUESTION_MARK)
.setCallback((x, y, z) -> {
playClickSound(getPlayer());
if (y.isRight) {
doSearch("");
} else if (y.isLeft) {
SearchScreen searchScreen = new SearchScreen(this, "");
searchScreen.open();
}
})
);
}
}

@ -0,0 +1,24 @@
{
"gui.servershop.shop.title": "Shop",
"gui.servershop.item.buyprice": "Buy for %d",
"gui.servershop.item.sellprice": "Sell for %d",
"message.servershop.storage.load": "Loaded prices from disk",
"message.servershop.storage.save": "Saved prices to disk",
"message.servershop.storage.save_fail": "Failed saving prices to disk",
"message.servershop.storage.reset": "Reset all prices to 0",
"message.servershop.storage.set": "Set price for %s, buy price: is %d and sell price is: %d",
"message.servershop.storage.set_fail": "Setting price for %s failed",
"message.servershop.pay.success": "You paid %d to %s, now you have %d",
"message.servershop.pay.bad_amount": "%d is more than you have(%d), or an invalid amount",
"message.servershop.balance.other": "%s has %d on his account",
"message.servershop.balance.self": "You have %d on your account",
"message.servershop.balance.set_self": "Your balance was set to %d",
"message.servershop.balance.set_other": "%s now has %d on his account",
"message.servershop.buy.not_enough": "You don't have enough money (%d>%d), you need %d more",
"message.servershop.buy.inventory": "You don't have enough inventory space",
"message.servershop.buy.success": "You bought %d %s for %d",
"message.servershop.buy.not_available": "This item (%s) is not available for buying",
"message.servershop.sell.not_enough": "You don't have this item",
"message.servershop.sell.success": "You sold %d %s for %d",
"message.servershop.sell.not_available": "This item (%s) is not available for sale"
}

@ -0,0 +1,25 @@
{
"schemaVersion": 1,
"id": "servershop",
"version": "${version}",
"name": "ServerShop",
"description": "",
"authors": [],
"contact": {},
"license": "MIT",
"icon": "assets/servershop/icon.png",
"environment": "*",
"entrypoints": {
"main": [
"systems.brn.servershop.ServerShop"
]
},
"mixins": [
"servershop.mixins.json"
],
"depends": {
"fabricloader": ">=${loader_version}",
"fabric": "*",
"minecraft": "${minecraft_version}"
}
}

@ -0,0 +1,13 @@
{
"required": true,
"minVersion": "0.8",
"package": "systems.brn.servershop.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [
],
"client": [
],
"injectors": {
"defaultRequire": 1
}
}