Initial Commit

This commit is contained in:
everbarry 2026-03-25 12:06:48 +01:00
parent 4d6fa9c83d
commit e90f127b03
27 changed files with 1528 additions and 0 deletions

5
.gitattributes vendored Normal file
View File

@ -0,0 +1,5 @@
# Disable autocrlf on generated files, they always generate with LF
# Add any extra files or paths here to make git stop saying they
# are changed when only line endings change.
src/generated/**/.cache/cache text eol=lf
src/generated/**/*.json text eol=lf

207
build.gradle Normal file
View File

@ -0,0 +1,207 @@
plugins {
id 'eclipse'
id 'idea'
id 'maven-publish'
id 'net.minecraftforge.gradle' version '[6.0,6.2)'
}
version = mod_version
group = mod_group_id
base {
archivesName = mod_id
}
// Mojang ships Java 17 to end users in 1.18+, so your mod should target Java 17.
java.toolchain.languageVersion = JavaLanguageVersion.of(17)
println "Java: ${System.getProperty 'java.version'}, JVM: ${System.getProperty 'java.vm.version'} (${System.getProperty 'java.vendor'}), Arch: ${System.getProperty 'os.arch'}"
minecraft {
// The mappings can be changed at any time and must be in the following format.
// Channel: Version:
// official MCVersion Official field/method names from Mojang mapping files
// parchment YYYY.MM.DD-MCVersion Open community-sourced parameter names and javadocs layered on top of official
//
// You must be aware of the Mojang license when using the 'official' or 'parchment' mappings.
// See more information here: https://github.com/MinecraftForge/MCPConfig/blob/master/Mojang.md
//
// Parchment is an unofficial project maintained by ParchmentMC, separate from MinecraftForge
// Additional setup is needed to use their mappings: https://parchmentmc.org/docs/getting-started
//
// Use non-default mappings at your own risk. They may not always work.
// Simply re-run your setup task after changing the mappings to update your workspace.
mappings channel: mapping_channel, version: mapping_version
// When true, this property will have all Eclipse/IntelliJ IDEA run configurations run the "prepareX" task for the given run configuration before launching the game.
// In most cases, it is not necessary to enable.
// enableEclipsePrepareRuns = true
// enableIdeaPrepareRuns = true
// This property allows configuring Gradle's ProcessResources task(s) to run on IDE output locations before launching the game.
// It is REQUIRED to be set to true for this template to function.
// See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html
copyIdeResources = true
// When true, this property will add the folder name of all declared run configurations to generated IDE run configurations.
// The folder name can be set on a run configuration using the "folderName" property.
// By default, the folder name of a run configuration is the name of the Gradle project containing it.
// generateRunFolders = true
// This property enables access transformers for use in development.
// They will be applied to the Minecraft artifact.
// The access transformer file can be anywhere in the project.
// However, it must be at "META-INF/accesstransformer.cfg" in the final mod jar to be loaded by Forge.
// This default location is a best practice to automatically put the file in the right place in the final jar.
// See https://docs.minecraftforge.net/en/latest/advanced/accesstransformers/ for more information.
// accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg')
// Default run configurations.
// These can be tweaked, removed, or duplicated as needed.
runs {
// applies to all the run configs below
configureEach {
workingDirectory project.file('run')
// Recommended logging data for a userdev environment
// The markers can be added/remove as needed separated by commas.
// "SCAN": For mods scan.
// "REGISTRIES": For firing of registry events.
// "REGISTRYDUMP": For getting the contents of all registries.
property 'forge.logging.markers', 'REGISTRIES'
// Recommended logging level for the console
// You can set various levels here.
// Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels
property 'forge.logging.console.level', 'debug'
mods {
"${mod_id}" {
source sourceSets.main
}
}
}
client {
// Comma-separated list of namespaces to load gametests from. Empty = all namespaces.
property 'forge.enabledGameTestNamespaces', mod_id
}
server {
property 'forge.enabledGameTestNamespaces', mod_id
args '--nogui'
}
// This run config launches GameTestServer and runs all registered gametests, then exits.
// By default, the server will crash when no gametests are provided.
// The gametest system is also enabled by default for other run configs under the /test command.
gameTestServer {
property 'forge.enabledGameTestNamespaces', mod_id
}
data {
// example of overriding the workingDirectory set in configureEach above
workingDirectory project.file('run-data')
// Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources.
args '--mod', mod_id, '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/')
}
}
}
// Include resources generated by data generators.
sourceSets.main.resources { srcDir 'src/generated/resources' }
repositories {
// Put repositories for dependencies here
// ForgeGradle automatically adds the Forge maven and Maven Central for you
// If you have mod jar dependencies in ./libs, you can declare them as a repository like so.
// See https://docs.gradle.org/current/userguide/declaring_repositories.html#sub:flat_dir_resolver
// flatDir {
// dir 'libs'
// }
}
dependencies {
// Specify the version of Minecraft to use.
// Any artifact can be supplied so long as it has a "userdev" classifier artifact and is a compatible patcher artifact.
// The "userdev" classifier will be requested and setup by ForgeGradle.
// If the group id is "net.minecraft" and the artifact id is one of ["client", "server", "joined"],
// then special handling is done to allow a setup of a vanilla dependency without the use of an external repository.
minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}"
// Example mod dependency with JEI - using fg.deobf() ensures the dependency is remapped to your development mappings
// The JEI API is declared for compile time use, while the full JEI artifact is used at runtime
// compileOnly fg.deobf("mezz.jei:jei-${mc_version}-common-api:${jei_version}")
// compileOnly fg.deobf("mezz.jei:jei-${mc_version}-forge-api:${jei_version}")
// runtimeOnly fg.deobf("mezz.jei:jei-${mc_version}-forge:${jei_version}")
// Example mod dependency using a mod jar from ./libs with a flat dir repository
// This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar
// The group id is ignored when searching -- in this case, it is "blank"
// implementation fg.deobf("blank:coolmod-${mc_version}:${coolmod_version}")
// For more info:
// http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html
// http://www.gradle.org/docs/current/userguide/dependency_management.html
}
// This block of code expands all declared replace properties in the specified resource targets.
// A missing property will result in an error. Properties are expanded using ${} Groovy notation.
// When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments.
// See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html
tasks.named('processResources', ProcessResources).configure {
var replaceProperties = [
minecraft_version: minecraft_version, minecraft_version_range: minecraft_version_range,
forge_version: forge_version, forge_version_range: forge_version_range,
loader_version_range: loader_version_range,
mod_id: mod_id, mod_name: mod_name, mod_license: mod_license, mod_version: mod_version,
mod_authors: mod_authors, mod_description: mod_description,
]
inputs.properties replaceProperties
filesMatching(['META-INF/mods.toml', 'pack.mcmeta']) {
expand replaceProperties + [project: project]
}
}
// Example for how to get properties into the manifest for reading at runtime.
tasks.named('jar', Jar).configure {
manifest {
attributes([
'Specification-Title' : mod_id,
'Specification-Vendor' : mod_authors,
'Specification-Version' : '1', // We are version 1 of ourselves
'Implementation-Title' : project.name,
'Implementation-Version' : project.jar.archiveVersion,
'Implementation-Vendor' : mod_authors,
'Implementation-Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")
])
}
// This is the preferred method to reobfuscate your jar file
finalizedBy 'reobfJar'
}
// However if you are in a multi-project build, dev time needs unobfed jar files, so you can delay the obfuscation until publishing by doing:
// tasks.named('publish').configure {
// dependsOn 'reobfJar'
// }
// Example configuration to allow publishing using the maven-publish plugin
publishing {
publications {
register('mavenJava', MavenPublication) {
artifact jar
}
}
repositories {
maven {
url "file://${project.projectDir}/mcmodsrepo"
}
}
}
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation
}

18
gradle.properties Normal file
View File

@ -0,0 +1,18 @@
org.gradle.jvmargs=-Xmx3G
org.gradle.daemon=false
minecraft_version=1.20.1
minecraft_version_range=[1.20.1,1.21)
forge_version=47.4.10
forge_version_range=[47,)
loader_version_range=[47,)
mapping_channel=official
mapping_version=1.20.1
mod_id=rustybeds
mod_name=Rusty Beds
mod_license=MIT
mod_version=1.0.0
mod_group_id=com.rustybeds
mod_authors=Barry
mod_description=Track your last 5 beds and choose where to respawn when you die.

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

Binary file not shown.

View File

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

245
gradlew vendored Executable file
View File

@ -0,0 +1,245 @@
#!/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/subprojects/plugins/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##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && 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
which java >/dev/null 2>&1 || 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
# 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=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=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, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
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
View 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.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
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

13
settings.gradle Normal file
View File

@ -0,0 +1,13 @@
pluginManagement {
repositories {
gradlePluginPortal()
maven {
name = 'MinecraftForge'
url = 'https://maven.minecraftforge.net/'
}
}
}
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0'
}

View File

@ -0,0 +1,20 @@
package com.rustybeds;
import com.rustybeds.network.ModNetwork;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
@Mod(RustyBeds.MOD_ID)
public class RustyBeds {
public static final String MOD_ID = "rustybeds";
public RustyBeds() {
FMLJavaModLoadingContext.get().getModEventBus().addListener(this::commonSetup);
}
private void commonSetup(FMLCommonSetupEvent event) {
event.enqueueWork(ModNetwork::register);
}
}

View File

@ -0,0 +1,84 @@
package com.rustybeds.capability;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.Level;
public class BedEntry {
private final BlockPos pos;
private final ResourceKey<Level> dimension;
private String name;
private boolean locked;
public BedEntry(BlockPos pos, ResourceKey<Level> dimension) {
this(pos, dimension, "", false);
}
public BedEntry(BlockPos pos, ResourceKey<Level> dimension, String name, boolean locked) {
this.pos = pos;
this.dimension = dimension;
this.name = name != null ? name : "";
this.locked = locked;
}
public BlockPos pos() { return pos; }
public ResourceKey<Level> dimension() { return dimension; }
public String name() { return name; }
public boolean locked() { return locked; }
public void setName(String name) { this.name = name != null ? name : ""; }
public void setLocked(boolean locked) { this.locked = locked; }
public boolean matches(BlockPos otherPos, ResourceKey<Level> otherDim) {
return pos.equals(otherPos) && dimension.equals(otherDim);
}
public CompoundTag toNbt() {
CompoundTag tag = new CompoundTag();
tag.putInt("x", pos.getX());
tag.putInt("y", pos.getY());
tag.putInt("z", pos.getZ());
tag.putString("dim", dimension.location().toString());
tag.putString("name", name);
tag.putBoolean("locked", locked);
return tag;
}
public static BedEntry fromNbt(CompoundTag tag) {
BlockPos pos = new BlockPos(tag.getInt("x"), tag.getInt("y"), tag.getInt("z"));
ResourceKey<Level> dim = ResourceKey.create(Registries.DIMENSION,
new ResourceLocation(tag.getString("dim")));
String name = tag.getString("name");
boolean locked = tag.getBoolean("locked");
return new BedEntry(pos, dim, name, locked);
}
public void encode(FriendlyByteBuf buf) {
buf.writeBlockPos(pos);
buf.writeResourceLocation(dimension.location());
buf.writeUtf(name);
buf.writeBoolean(locked);
}
public static BedEntry decode(FriendlyByteBuf buf) {
BlockPos pos = buf.readBlockPos();
ResourceLocation dim = buf.readResourceLocation();
String name = buf.readUtf();
boolean locked = buf.readBoolean();
return new BedEntry(pos, ResourceKey.create(Registries.DIMENSION, dim), name, locked);
}
public String dimensionDisplayName() {
String path = dimension.location().getPath();
return switch (path) {
case "overworld" -> "Overworld";
case "the_nether" -> "The Nether";
case "the_end" -> "The End";
default -> dimension.location().toString();
};
}
}

View File

@ -0,0 +1,119 @@
package com.rustybeds.capability;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BedBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.CapabilityManager;
import net.minecraftforge.common.capabilities.CapabilityToken;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
public class BedHistory {
public static final Capability<BedHistory> CAPABILITY = CapabilityManager.get(new CapabilityToken<>() {});
private static final int MAX_BEDS = 5;
private final List<BedEntry> beds = new ArrayList<>();
public void addBed(BedEntry entry) {
for (int i = 0; i < beds.size(); i++) {
BedEntry existing = beds.get(i);
if (existing.matches(entry.pos(), entry.dimension())) {
beds.remove(i);
beds.add(0, existing);
return;
}
}
if (beds.size() < MAX_BEDS) {
beds.add(0, entry);
return;
}
for (int i = beds.size() - 1; i >= 0; i--) {
if (!beds.get(i).locked()) {
beds.remove(i);
beds.add(0, entry);
return;
}
}
}
public void removeBed(BlockPos pos, ResourceKey<Level> dimension) {
beds.removeIf(e -> e.matches(pos, dimension));
}
public void renameBed(BlockPos pos, ResourceKey<Level> dimension, String name) {
for (BedEntry e : beds) {
if (e.matches(pos, dimension)) {
e.setName(name);
return;
}
}
}
public void toggleLock(BlockPos pos, ResourceKey<Level> dimension) {
for (BedEntry e : beds) {
if (e.matches(pos, dimension)) {
e.setLocked(!e.locked());
return;
}
}
}
public List<BedEntry> getBeds() {
return Collections.unmodifiableList(beds);
}
public List<BedEntry> getValidBeds(MinecraftServer server) {
List<BedEntry> valid = new ArrayList<>();
Iterator<BedEntry> iter = beds.iterator();
while (iter.hasNext()) {
BedEntry entry = iter.next();
ServerLevel level = server.getLevel(entry.dimension());
if (level != null) {
BlockState state = level.getBlockState(entry.pos());
if (state.getBlock() instanceof BedBlock) {
valid.add(entry);
} else {
iter.remove();
}
}
}
return valid;
}
public void copyFrom(BedHistory other) {
beds.clear();
beds.addAll(other.beds);
}
public CompoundTag serializeNBT() {
CompoundTag tag = new CompoundTag();
ListTag list = new ListTag();
for (BedEntry entry : beds) {
list.add(entry.toNbt());
}
tag.put("beds", list);
return tag;
}
public void deserializeNBT(CompoundTag tag) {
beds.clear();
ListTag list = tag.getList("beds", Tag.TAG_COMPOUND);
for (int i = 0; i < list.size(); i++) {
beds.add(BedEntry.fromNbt(list.getCompound(i)));
}
}
}

View File

@ -0,0 +1,34 @@
package com.rustybeds.capability;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.ICapabilitySerializable;
import net.minecraftforge.common.util.LazyOptional;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class BedHistoryProvider implements ICapabilitySerializable<CompoundTag> {
private final BedHistory bedHistory = new BedHistory();
private final LazyOptional<BedHistory> lazy = LazyOptional.of(() -> bedHistory);
@Override
public @NotNull <T> LazyOptional<T> getCapability(@NotNull Capability<T> cap, @Nullable Direction side) {
return cap == BedHistory.CAPABILITY ? lazy.cast() : LazyOptional.empty();
}
@Override
public CompoundTag serializeNBT() {
return bedHistory.serializeNBT();
}
@Override
public void deserializeNBT(CompoundTag tag) {
bedHistory.deserializeNBT(tag);
}
public void invalidate() {
lazy.invalidate();
}
}

View File

@ -0,0 +1,199 @@
package com.rustybeds.client;
import com.rustybeds.capability.BedEntry;
import com.rustybeds.network.ModNetwork;
import com.rustybeds.network.ModifyBedPacket;
import com.rustybeds.network.SelectBedPacket;
import net.minecraft.ChatFormatting;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import org.lwjgl.glfw.GLFW;
import java.util.ArrayList;
import java.util.List;
public class BedPickerScreen extends Screen {
private static final int MAIN_W = 230;
private static final int SMALL_W = 20;
private static final int BTN_H = 20;
private static final int GAP = 2;
private static final int ROW_GAP = 4;
private final List<BedEntry> beds;
private final boolean editOnly;
private int editingIndex = -1;
private EditBox editBox;
public BedPickerScreen(List<BedEntry> beds, boolean editOnly) {
super(Component.literal(editOnly ? "Manage Beds" : "Choose Your Respawn Bed"));
this.beds = new ArrayList<>(beds);
this.editOnly = editOnly;
}
@Override
protected void init() {
editBox = null;
int rowWidth = MAIN_W + 3 * (SMALL_W + GAP);
int rowLeft = (this.width - rowWidth) / 2;
int totalHeight = beds.size() * (BTN_H + ROW_GAP) + BTN_H + 20;
int startY = Math.max(55, (this.height - totalHeight) / 2);
for (int i = 0; i < beds.size(); i++) {
BedEntry bed = beds.get(i);
int y = startY + i * (BTN_H + ROW_GAP);
int x = rowLeft;
final int idx = i;
if (editingIndex == i) {
editBox = new EditBox(this.font, x, y, MAIN_W, BTN_H, Component.literal("Name"));
editBox.setMaxLength(32);
editBox.setValue(bed.name());
addRenderableWidget(editBox);
this.setInitialFocus(editBox);
x += MAIN_W + GAP;
addRenderableWidget(Button.builder(Component.literal("Ok"), btn -> confirmRename(idx))
.bounds(x, y, SMALL_W, BTN_H).build());
} else if (editOnly) {
Button labelBtn = Button.builder(buildLabel(bed, i == 0), btn -> {})
.bounds(x, y, MAIN_W, BTN_H).build();
labelBtn.active = false;
addRenderableWidget(labelBtn);
x += MAIN_W + GAP;
addRenderableWidget(Button.builder(Component.literal("E"), btn -> startRename(idx))
.bounds(x, y, SMALL_W, BTN_H).build());
} else {
addRenderableWidget(Button.builder(buildLabel(bed, i == 0), btn -> {
ModNetwork.sendToServer(new SelectBedPacket(idx));
onClose();
}).bounds(x, y, MAIN_W, BTN_H).build());
x += MAIN_W + GAP;
addRenderableWidget(Button.builder(Component.literal("E"), btn -> startRename(idx))
.bounds(x, y, SMALL_W, BTN_H).build());
}
x += SMALL_W + GAP;
Component lockLabel = bed.locked()
? Component.literal("L").withStyle(ChatFormatting.GREEN)
: Component.literal("L");
addRenderableWidget(Button.builder(lockLabel, btn -> toggleLock(idx))
.bounds(x, y, SMALL_W, BTN_H).build());
x += SMALL_W + GAP;
addRenderableWidget(Button.builder(
Component.literal("X").withStyle(ChatFormatting.RED), btn -> removeBed(idx))
.bounds(x, y, SMALL_W, BTN_H).build());
}
int stayY = startY + beds.size() * (BTN_H + ROW_GAP) + 12;
Component closeLabel = editOnly ? Component.literal("Done") : Component.literal("Stay Here");
addRenderableWidget(Button.builder(closeLabel, btn -> onClose())
.bounds(centerX(120), stayY, 120, BTN_H).build());
}
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
renderBackground(graphics);
graphics.drawCenteredString(this.font, this.title, this.width / 2, 20, 0xFFFFFF);
Component hint = editOnly
? Component.literal("E = rename L = lock X = remove")
: Component.literal("\u2605 = most recent ")
.append(Component.literal("L").withStyle(ChatFormatting.GREEN))
.append(Component.literal(" = locked"));
graphics.drawCenteredString(this.font, hint, this.width / 2, 32, 0xAAAAAA);
super.render(graphics, mouseX, mouseY, partialTick);
}
@Override
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
if (editingIndex >= 0) {
if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) {
confirmRename(editingIndex);
return true;
}
if (keyCode == GLFW.GLFW_KEY_ESCAPE) {
editingIndex = -1;
rebuildWidgets();
return true;
}
}
return super.keyPressed(keyCode, scanCode, modifiers);
}
@Override
public boolean isPauseScreen() {
return false;
}
private Component buildLabel(BedEntry bed, boolean mostRecent) {
MutableComponent label = Component.empty().copy();
if (bed.locked()) {
label.append(Component.literal("[L] ").withStyle(ChatFormatting.GREEN));
}
if (!bed.name().isEmpty()) {
label.append(bed.name() + " ");
}
label.append(Component.literal(
bed.dimensionDisplayName() + " (" +
bed.pos().getX() + ", " + bed.pos().getY() + ", " + bed.pos().getZ() + ")")
.withStyle(ChatFormatting.GRAY));
if (mostRecent) {
label.append(Component.literal(" \u2605").withStyle(ChatFormatting.GOLD));
}
return label;
}
private void startRename(int index) {
editingIndex = index;
rebuildWidgets();
}
private void confirmRename(int index) {
if (editBox != null && index >= 0 && index < beds.size()) {
String newName = editBox.getValue().trim();
BedEntry bed = beds.get(index);
bed.setName(newName);
ModNetwork.sendToServer(new ModifyBedPacket(
ModifyBedPacket.Action.RENAME, bed.pos(), bed.dimension(), newName));
}
editingIndex = -1;
rebuildWidgets();
}
private void toggleLock(int index) {
editingIndex = -1;
if (index >= 0 && index < beds.size()) {
BedEntry bed = beds.get(index);
bed.setLocked(!bed.locked());
ModNetwork.sendToServer(new ModifyBedPacket(
ModifyBedPacket.Action.TOGGLE_LOCK, bed.pos(), bed.dimension()));
}
rebuildWidgets();
}
private void removeBed(int index) {
editingIndex = -1;
if (index >= 0 && index < beds.size()) {
BedEntry bed = beds.get(index);
ModNetwork.sendToServer(new ModifyBedPacket(
ModifyBedPacket.Action.REMOVE, bed.pos(), bed.dimension()));
beds.remove(index);
}
if (beds.isEmpty()) {
onClose();
return;
}
rebuildWidgets();
}
private int centerX(int elementWidth) {
return (this.width - elementWidth) / 2;
}
}

View File

@ -0,0 +1,26 @@
package com.rustybeds.client;
import com.rustybeds.RustyBeds;
import com.rustybeds.network.ModNetwork;
import com.rustybeds.network.RequestBedPickerPacket;
import net.minecraft.client.Minecraft;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
@Mod.EventBusSubscriber(modid = RustyBeds.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT)
public class ClientForgeEvents {
@SubscribeEvent
public static void onClientTick(TickEvent.ClientTickEvent event) {
if (event.phase != TickEvent.Phase.END) return;
Minecraft mc = Minecraft.getInstance();
if (mc.screen != null || mc.player == null) return;
while (RustyBedsKeybinds.OPEN_BED_MENU.consumeClick()) {
ModNetwork.sendToServer(RequestBedPickerPacket.INSTANCE);
}
}
}

View File

@ -0,0 +1,16 @@
package com.rustybeds.client;
import com.rustybeds.RustyBeds;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.RegisterKeyMappingsEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
@Mod.EventBusSubscriber(modid = RustyBeds.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
public class ClientModEvents {
@SubscribeEvent
public static void onRegisterKeyMappings(RegisterKeyMappingsEvent event) {
event.register(RustyBedsKeybinds.OPEN_BED_MENU);
}
}

View File

@ -0,0 +1,13 @@
package com.rustybeds.client;
import com.rustybeds.capability.BedEntry;
import net.minecraft.client.Minecraft;
import java.util.List;
public class ClientPacketHandler {
public static void openBedPicker(List<BedEntry> beds, boolean editOnly) {
Minecraft.getInstance().setScreen(new BedPickerScreen(beds, editOnly));
}
}

View File

@ -0,0 +1,20 @@
package com.rustybeds.client;
import com.mojang.blaze3d.platform.InputConstants;
import com.rustybeds.RustyBeds;
import net.minecraft.client.KeyMapping;
import net.minecraftforge.client.settings.KeyConflictContext;
import org.lwjgl.glfw.GLFW;
public final class RustyBedsKeybinds {
public static final KeyMapping OPEN_BED_MENU = new KeyMapping(
"key." + RustyBeds.MOD_ID + ".open_bed_menu",
KeyConflictContext.IN_GAME,
InputConstants.Type.KEYSYM,
GLFW.GLFW_KEY_B,
"key.categories." + RustyBeds.MOD_ID
);
private RustyBedsKeybinds() {}
}

View File

@ -0,0 +1,98 @@
package com.rustybeds.event;
import com.rustybeds.RustyBeds;
import com.rustybeds.capability.BedEntry;
import com.rustybeds.capability.BedHistory;
import com.rustybeds.capability.BedHistoryProvider;
import com.rustybeds.network.ModNetwork;
import com.rustybeds.network.OpenBedPickerPacket;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraftforge.event.AttachCapabilitiesEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.event.entity.player.PlayerSleepInBedEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.server.ServerLifecycleHooks;
import java.util.*;
@Mod.EventBusSubscriber(modid = RustyBeds.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE)
public class ForgeBusEvents {
private static final ResourceLocation CAP_ID = new ResourceLocation(RustyBeds.MOD_ID, "bed_history");
private record PendingBed(BlockPos pos, ResourceKey<Level> dimension) {}
private static final Map<UUID, PendingBed> pendingBeds = new HashMap<>();
@SubscribeEvent
public static void onAttachCapabilities(AttachCapabilitiesEvent<Entity> event) {
if (event.getObject() instanceof Player) {
event.addCapability(CAP_ID, new BedHistoryProvider());
}
}
@SubscribeEvent
public static void onPlayerClone(PlayerEvent.Clone event) {
event.getOriginal().reviveCaps();
try {
event.getOriginal().getCapability(BedHistory.CAPABILITY).ifPresent(oldCap ->
event.getEntity().getCapability(BedHistory.CAPABILITY).ifPresent(newCap ->
newCap.copyFrom(oldCap)
)
);
} finally {
event.getOriginal().invalidateCaps();
}
}
@SubscribeEvent
public static void onPlayerSleep(PlayerSleepInBedEvent event) {
if (!(event.getEntity() instanceof ServerPlayer player)) return;
if (event.getResultStatus() != null) return;
pendingBeds.put(player.getUUID(),
new PendingBed(event.getPos(), player.level().dimension()));
}
@SubscribeEvent
public static void onServerTick(TickEvent.ServerTickEvent event) {
if (event.phase != TickEvent.Phase.END || pendingBeds.isEmpty()) return;
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
if (server == null) return;
Iterator<Map.Entry<UUID, PendingBed>> iter = pendingBeds.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<UUID, PendingBed> entry = iter.next();
iter.remove();
ServerPlayer player = server.getPlayerList().getPlayer(entry.getKey());
if (player != null && player.isSleeping()) {
PendingBed bed = entry.getValue();
player.getCapability(BedHistory.CAPABILITY).ifPresent(cap ->
cap.addBed(new BedEntry(bed.pos(), bed.dimension()))
);
}
}
}
@SubscribeEvent
public static void onPlayerRespawn(PlayerEvent.PlayerRespawnEvent event) {
if (!(event.getEntity() instanceof ServerPlayer player)) return;
if (event.isEndConquered()) return;
player.getCapability(BedHistory.CAPABILITY).ifPresent(cap -> {
List<BedEntry> validBeds = cap.getValidBeds(player.getServer());
if (validBeds.size() > 1) {
ModNetwork.sendToPlayer(new OpenBedPickerPacket(validBeds), player);
}
});
}
}

View File

@ -0,0 +1,16 @@
package com.rustybeds.event;
import com.rustybeds.RustyBeds;
import com.rustybeds.capability.BedHistory;
import net.minecraftforge.common.capabilities.RegisterCapabilitiesEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
@Mod.EventBusSubscriber(modid = RustyBeds.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD)
public class ModBusEvents {
@SubscribeEvent
public static void onRegisterCapabilities(RegisterCapabilitiesEvent event) {
event.register(BedHistory.class);
}
}

View File

@ -0,0 +1,57 @@
package com.rustybeds.network;
import com.rustybeds.RustyBeds;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkDirection;
import net.minecraftforge.network.NetworkRegistry;
import net.minecraftforge.network.PacketDistributor;
import net.minecraftforge.network.simple.SimpleChannel;
public class ModNetwork {
private static final String PROTOCOL = "4";
private static SimpleChannel CHANNEL;
private static int id = 0;
public static void register() {
CHANNEL = NetworkRegistry.ChannelBuilder
.named(new ResourceLocation(RustyBeds.MOD_ID, "main"))
.networkProtocolVersion(() -> PROTOCOL)
.clientAcceptedVersions(PROTOCOL::equals)
.serverAcceptedVersions(PROTOCOL::equals)
.simpleChannel();
CHANNEL.messageBuilder(OpenBedPickerPacket.class, id++, NetworkDirection.PLAY_TO_CLIENT)
.encoder(OpenBedPickerPacket::encode)
.decoder(OpenBedPickerPacket::decode)
.consumerMainThread(OpenBedPickerPacket::handle)
.add();
CHANNEL.messageBuilder(SelectBedPacket.class, id++, NetworkDirection.PLAY_TO_SERVER)
.encoder(SelectBedPacket::encode)
.decoder(SelectBedPacket::decode)
.consumerMainThread(SelectBedPacket::handle)
.add();
CHANNEL.messageBuilder(ModifyBedPacket.class, id++, NetworkDirection.PLAY_TO_SERVER)
.encoder(ModifyBedPacket::encode)
.decoder(ModifyBedPacket::decode)
.consumerMainThread(ModifyBedPacket::handle)
.add();
CHANNEL.messageBuilder(RequestBedPickerPacket.class, id++, NetworkDirection.PLAY_TO_SERVER)
.encoder(RequestBedPickerPacket::encode)
.decoder(RequestBedPickerPacket::decode)
.consumerMainThread(RequestBedPickerPacket::handle)
.add();
}
public static void sendToPlayer(Object msg, ServerPlayer player) {
CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), msg);
}
public static void sendToServer(Object msg) {
CHANNEL.sendToServer(msg);
}
}

View File

@ -0,0 +1,63 @@
package com.rustybeds.network;
import com.rustybeds.capability.BedHistory;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.Level;
import net.minecraftforge.network.NetworkEvent;
import java.util.function.Supplier;
public class ModifyBedPacket {
public enum Action { REMOVE, RENAME, TOGGLE_LOCK }
private final Action action;
private final BlockPos pos;
private final ResourceKey<Level> dimension;
private final String name;
public ModifyBedPacket(Action action, BlockPos pos, ResourceKey<Level> dimension) {
this(action, pos, dimension, "");
}
public ModifyBedPacket(Action action, BlockPos pos, ResourceKey<Level> dimension, String name) {
this.action = action;
this.pos = pos;
this.dimension = dimension;
this.name = name;
}
public static void encode(ModifyBedPacket msg, FriendlyByteBuf buf) {
buf.writeByte(msg.action.ordinal());
buf.writeBlockPos(msg.pos);
buf.writeResourceLocation(msg.dimension.location());
buf.writeUtf(msg.name);
}
public static ModifyBedPacket decode(FriendlyByteBuf buf) {
Action action = Action.values()[buf.readByte()];
BlockPos pos = buf.readBlockPos();
ResourceLocation dim = buf.readResourceLocation();
String name = buf.readUtf();
return new ModifyBedPacket(action, pos, ResourceKey.create(Registries.DIMENSION, dim), name);
}
public static void handle(ModifyBedPacket msg, Supplier<NetworkEvent.Context> ctx) {
ServerPlayer player = ctx.get().getSender();
if (player == null) return;
player.getCapability(BedHistory.CAPABILITY).ifPresent(cap -> {
switch (msg.action) {
case REMOVE -> cap.removeBed(msg.pos, msg.dimension);
case RENAME -> cap.renameBed(msg.pos, msg.dimension, msg.name);
case TOGGLE_LOCK -> cap.toggleLock(msg.pos, msg.dimension);
}
});
ctx.get().setPacketHandled(true);
}
}

View File

@ -0,0 +1,52 @@
package com.rustybeds.network;
import com.rustybeds.capability.BedEntry;
import com.rustybeds.client.ClientPacketHandler;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.fml.DistExecutor;
import net.minecraftforge.network.NetworkEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public class OpenBedPickerPacket {
private final List<BedEntry> beds;
/** If true (keybind menu): rename / lock / remove only — no teleport. */
private final boolean editOnly;
public OpenBedPickerPacket(List<BedEntry> beds) {
this(beds, false);
}
public OpenBedPickerPacket(List<BedEntry> beds, boolean editOnly) {
this.beds = beds;
this.editOnly = editOnly;
}
public static void encode(OpenBedPickerPacket msg, FriendlyByteBuf buf) {
buf.writeBoolean(msg.editOnly);
buf.writeInt(msg.beds.size());
for (BedEntry entry : msg.beds) {
entry.encode(buf);
}
}
public static OpenBedPickerPacket decode(FriendlyByteBuf buf) {
boolean editOnly = buf.readBoolean();
int size = buf.readInt();
List<BedEntry> beds = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
beds.add(BedEntry.decode(buf));
}
return new OpenBedPickerPacket(beds, editOnly);
}
public static void handle(OpenBedPickerPacket msg, Supplier<NetworkEvent.Context> ctx) {
DistExecutor.unsafeRunWhenOn(Dist.CLIENT,
() -> () -> ClientPacketHandler.openBedPicker(msg.beds, msg.editOnly));
ctx.get().setPacketHandled(true);
}
}

View File

@ -0,0 +1,37 @@
package com.rustybeds.network;
import com.rustybeds.capability.BedHistory;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
import java.util.function.Supplier;
public final class RequestBedPickerPacket {
public static final RequestBedPickerPacket INSTANCE = new RequestBedPickerPacket();
private RequestBedPickerPacket() {}
public static void encode(RequestBedPickerPacket msg, FriendlyByteBuf buf) {}
public static RequestBedPickerPacket decode(FriendlyByteBuf buf) {
return INSTANCE;
}
public static void handle(RequestBedPickerPacket msg, Supplier<NetworkEvent.Context> ctx) {
ServerPlayer player = ctx.get().getSender();
if (player == null) return;
player.getCapability(BedHistory.CAPABILITY).ifPresent(cap -> {
var beds = cap.getValidBeds(player.getServer());
if (beds.isEmpty()) {
player.sendSystemMessage(Component.literal("Rusty Beds: no beds stored yet."));
} else {
ModNetwork.sendToPlayer(new OpenBedPickerPacket(beds, true), player);
}
});
ctx.get().setPacketHandled(true);
}
}

View File

@ -0,0 +1,54 @@
package com.rustybeds.network;
import com.rustybeds.capability.BedEntry;
import com.rustybeds.capability.BedHistory;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.network.NetworkEvent;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
public class SelectBedPacket {
private final int index;
public SelectBedPacket(int index) {
this.index = index;
}
public static void encode(SelectBedPacket msg, FriendlyByteBuf buf) {
buf.writeInt(msg.index);
}
public static SelectBedPacket decode(FriendlyByteBuf buf) {
return new SelectBedPacket(buf.readInt());
}
public static void handle(SelectBedPacket msg, Supplier<NetworkEvent.Context> ctx) {
ServerPlayer player = ctx.get().getSender();
if (player == null) return;
player.getCapability(BedHistory.CAPABILITY).ifPresent(cap -> {
List<BedEntry> validBeds = cap.getValidBeds(player.getServer());
if (msg.index < 0 || msg.index >= validBeds.size()) return;
BedEntry bed = validBeds.get(msg.index);
ServerLevel level = player.getServer().getLevel(bed.dimension());
if (level == null) return;
Optional<Vec3> safePos = Player.findRespawnPositionAndUseSpawnBlock(
level, bed.pos(), 0F, false, true);
if (safePos.isPresent()) {
Vec3 p = safePos.get();
player.teleportTo(level, p.x, p.y, p.z, player.getYRot(), player.getXRot());
player.setRespawnPosition(bed.dimension(), bed.pos(), 0F, false, false);
}
});
ctx.get().setPacketHandled(true);
}
}

View File

@ -0,0 +1,24 @@
modLoader="javafml"
loaderVersion="${loader_version_range}"
license="${mod_license}"
[[mods]]
modId="${mod_id}"
version="${mod_version}"
displayName="${mod_name}"
authors="${mod_authors}"
description='''${mod_description}'''
[[dependencies.${mod_id}]]
modId="forge"
mandatory=true
versionRange="${forge_version_range}"
ordering="NONE"
side="BOTH"
[[dependencies.${mod_id}]]
modId="minecraft"
mandatory=true
versionRange="${minecraft_version_range}"
ordering="NONE"
side="BOTH"

View File

@ -0,0 +1,4 @@
{
"key.rustybeds.open_bed_menu": "Open bed menu",
"key.categories.rustybeds": "Rusty Beds"
}

View File

@ -0,0 +1,6 @@
{
"pack": {
"description": "Rusty Beds resources",
"pack_format": 15
}
}