Skip to content

Homes

Set homes and teleport back to them

Source: src/scripting/managed/samples/Homes/ · version 1.0.0. The code below is read from the repository at build time.

  • commands
  • Brigadier command trees (v1)
  • config (v1)
Terminal window
dotnet build src/scripting/managed/samples/Homes -c Release
# the output lands in build/bin/dotnet/samples/Homes/ when built through CMake (MC_BUILD_SCRIPTING);
# copy the dll into your plugins folder and start the server with --plugins
Homes.cs
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Homes;
/// <summary>Settings of the Homes plugin (<c>plugins/homes/config.json</c>, written with these defaults on first run).</summary>
public sealed class HomesConfig
{
/// <summary>How many homes a player may set (players with <c>homes.unlimited</c> are not limited).</summary>
public int MaxHomes { get; set; } = 3;
/// <summary>Seconds a player must wait between two teleports (players with <c>homes.cooldown.bypass</c> do not wait).</summary>
public int CooldownSeconds { get; set; } = 10;
/// <summary>The sound played to the player after teleporting; empty for none.</summary>
public string TeleportSound { get; set; } = "minecraft:entity.enderman.teleport";
}
/// <summary>One saved home.</summary>
public sealed class SavedHome
{
/// <summary>The dimension key.</summary>
public string World { get; set; } = "minecraft:overworld";
/// <summary>X.</summary>
public double X { get; set; }
/// <summary>Y.</summary>
public double Y { get; set; }
/// <summary>Z.</summary>
public double Z { get; set; }
/// <summary>Yaw.</summary>
public float Yaw { get; set; }
/// <summary>Pitch.</summary>
public float Pitch { get; set; }
}
/// <summary>The saved homes of every player (<c>homes.json</c>): profile id to home name to home.</summary>
public sealed class HomesData
{
/// <summary>Homes per player.</summary>
public Dictionary<string, Dictionary<string, SavedHome>> Players { get; set; } = [];
}
[JsonSerializable(typeof(HomesConfig))]
[JsonSerializable(typeof(HomesData))]
[JsonSourceGenerationOptions(WriteIndented = true)]
internal sealed partial class HomesJson : JsonSerializerContext;
/// <summary>Sample: <c>/sethome</c>, <c>/home</c>, <c>/delhome</c>, <c>/homes</c> with a typed config, permission nodes declared in <c>plugin.json</c>, a data file in the plugin's
/// data folder, tab completion of the player's own home names, a cooldown, cross-dimension teleporting and feedback with sound and titles.</summary>
[PluginInfo("Homes", Version = "1.0.0", Author = "Ramjet", Description = "Set homes and teleport back to them")]
public sealed class HomesPlugin : Plugin
{
private Config<HomesConfig> _config = null!;
private HomesData _data = new();
private readonly Dictionary<Uuid, ulong> _lastTeleport = [];
protected override void OnEnable()
{
_config = Config.Load(HomesJson.Default.HomesConfig);
if (Config.ReadText("homes.json") is { } json)
{
try
{
_data = JsonSerializer.Deserialize(json, HomesJson.Default.HomesData) ?? new HomesData();
// System.Text.Json builds ordinal dictionaries: home names compare without regard to case, as /home Base and /home base should
foreach (var key in _data.Players.Keys.ToList())
_data.Players[key] = new Dictionary<string, SavedHome>(_data.Players[key], StringComparer.OrdinalIgnoreCase);
}
catch (JsonException e) { Log.Error("homes.json is invalid, starting with no homes (the file is kept)", e); }
}
Commands.Register(Command.Literal("sethome").Permission("homes.use")
.Executes(ctx => SetHome(ctx, "home"))
.Then(Command.Argument("name", Arg.Word()).Suggests(OwnHomes).Executes(ctx => SetHome(ctx, ctx.GetString("name")))));
Commands.Register(Command.Literal("home").Permission("homes.use")
.Executes(ctx => GoHome(ctx, "home"))
.Then(Command.Argument("name", Arg.Word()).Suggests(OwnHomes).Executes(ctx => GoHome(ctx, ctx.GetString("name")))),
description: "Teleport to one of your homes");
Commands.Register(Command.Literal("delhome").Permission("homes.use")
.Then(Command.Argument("name", Arg.Word()).Suggests(OwnHomes).Executes(ctx =>
{
var player = ctx.RequirePlayer();
var homes = HomesOf(player, create: false);
if (homes is null || !homes.Remove(ctx.GetString("name"))) throw new CommandFailure($"You have no home called '{ctx.GetString("name")}'");
Save();
ctx.Reply($"Deleted home '{ctx.GetString("name")}'");
return 1;
})));
Commands.Register(Command.Literal("homes").Permission("homes.use").Executes(ctx =>
{
var player = ctx.RequirePlayer();
var homes = HomesOf(player, create: false);
if (homes is null || homes.Count == 0) throw new CommandFailure("You have no homes. Use /sethome <name>");
foreach (var (name, home) in homes.OrderBy(h => h.Key))
ctx.Reply(Component.Text($"{name}: ").Color(TextColor.Gold).Append(Component.Text($"{home.X:0} {home.Y:0} {home.Z:0} ({home.World})").Color(TextColor.Gray)));
return homes.Count;
}));
}
private IEnumerable<string> OwnHomes(CommandContext ctx)
=> ctx.Player is { } p && HomesOf(p, create: false) is { } homes ? homes.Keys.Order() : [];
private Dictionary<string, SavedHome>? HomesOf(Player player, bool create)
{
string key = player.Uuid.ToString();
if (_data.Players.TryGetValue(key, out var homes)) return homes;
return create ? _data.Players[key] = new Dictionary<string, SavedHome>(StringComparer.OrdinalIgnoreCase) : null;
}
private void Save() => Config.WriteText("homes.json", JsonSerializer.Serialize(_data, HomesJson.Default.HomesData));
private int SetHome(CommandContext ctx, string name)
{
var player = ctx.RequirePlayer();
var homes = HomesOf(player, create: true)!;
bool exists = homes.ContainsKey(name);
if (!exists && homes.Count >= _config.Value.MaxHomes && !player.HasPermission("homes.unlimited"))
throw new CommandFailure($"You can have at most {_config.Value.MaxHomes} homes. Delete one with /delhome <name>");
var at = player.Location;
homes[name] = new SavedHome { World = at.World.Key, X = at.X, Y = at.Y, Z = at.Z, Yaw = at.Yaw, Pitch = at.Pitch };
Save();
ctx.Reply(Component.Text(exists ? "Moved home " : "Set home ").Color(TextColor.Green).Append(Component.Text(name).Color(TextColor.Yellow)));
return 1;
}
private int GoHome(CommandContext ctx, string name)
{
var player = ctx.RequirePlayer();
var homes = HomesOf(player, create: false);
if (homes is null || !homes.TryGetValue(name, out var home))
throw new CommandFailure(name == "home" ? "You have no home yet. Use /sethome" : $"You have no home called '{name}'");
ulong now = Server.CurrentTick;
if (!player.HasPermission("homes.cooldown.bypass") && _lastTeleport.TryGetValue(player.Uuid, out ulong last))
{
long waitTicks = _config.Value.CooldownSeconds * 20L - (long)(now - last);
if (waitTicks > 0) throw new CommandFailure($"Wait {(waitTicks + 19) / 20} more seconds before teleporting again");
}
var world = Server.GetWorld(home.World) ?? player.World;
if (!player.Teleport(new Location(world, home.X, home.Y, home.Z, home.Yaw, home.Pitch)))
throw new CommandFailure("The teleport was cancelled");
_lastTeleport[player.Uuid] = now;
if (_config.Value.TeleportSound.Length > 0) player.PlaySound(_config.Value.TeleportSound, SoundCategory.Players);
player.ShowTitle(Component.Text("Home").Color(TextColor.Green), Component.Text(name).Color(TextColor.Yellow), TitleTimes.Default);
return 1;
}
}
Homes.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<McApiProject>$(MSBuildThisFileDirectory)..\..\Mc.Scripting.Api\Mc.Scripting.Api.csproj</McApiProject>
</PropertyGroup>
<Import Project="..\..\Mc.Scripting.Plugin.props" />
<PropertyGroup Condition="'$(McOutDir)' != ''">
<OutputPath>$(McOutDir)samples/Homes/</OutputPath>
</PropertyGroup>
<ItemGroup>
<None Include="plugin.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
plugin.json
{
"id": "homes",
"name": "Homes",
"version": "1.0.0",
"api": "1.0",
"authors": ["Ramjet"],
"description": "Set homes and teleport back to them",
"permissions": [
{ "node": "homes.use", "default": "true", "description": "Use /sethome, /home, /delhome and /homes" },
{ "node": "homes.unlimited", "default": "op:3", "description": "Set more homes than the configured maximum" },
{ "node": "homes.cooldown.bypass", "default": "op:3", "description": "Teleport without waiting for the cooldown" }
]
}