Source code
SimpleClan runs on your server, so you should be able to read exactly what it does first. This is every file in the mod — the same code that builds the jar you download (version 1.2.2). The source files are also availble on GitHub.
It is here for you to read and check, not to copy. SimpleClan is All Rights Reserved: you may run it on your server, but not reuse, modify or republish the code. See Install to get started.
SimpleClan.java
src/main/java/com/simpleclan/SimpleClan.java
package com.simpleclan;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.context.CommandContext;
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.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.scores.PlayerTeam;
import net.minecraft.world.scores.Scoreboard;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static net.minecraft.commands.Commands.argument;
import static net.minecraft.commands.Commands.literal;
public class SimpleClan implements ModInitializer {
private static final Map<String, Clan> clans = new HashMap<>();
private static final Map<UUID, String> playerClans = new HashMap<>();
private static final Map<UUID, UUID> pendingInvites = new HashMap<>();
/** Clan key for each pending invite, so operator invites land in the right clan. */
private static final Map<UUID, String> pendingInviteClans = new HashMap<>();
private static final Map<String, List<UUID>> joinRequests = new HashMap<>();
private static final Gson gson = new GsonBuilder().setPrettyPrinting().create();
private static Path dataFolder;
private static ClanConfig config = new ClanConfig();
/** Last JSON written per data file, so the periodic save only writes what changed. */
private static final Map<String, String> lastWritten = new HashMap<>();
private int ticksUntilSave = 200;
/** Players who joined recently and get their team applied once more, a second later. */
private final Map<UUID, Integer> teamRecheck = new HashMap<>();
@Override
public void onInitialize() {
System.out.println("[SimpleClan] Fabric mod initializing...");
config = ClanConfig.load();
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> this.registerCommands(dispatcher));
ServerLifecycleEvents.SERVER_STOPPING.register(server -> {
System.out.println("[SimpleClan] Server stopping. Saving clan data...");
this.saveAllData();
});
ServerLifecycleEvents.SERVER_STARTED.register(server -> {
dataFolder = Paths.get("clan_data");
this.loadAllData();
this.restoreAllTeams(server);
this.rebuildPlayerClansMap();
});
ServerPlayConnectionEvents.JOIN.register((handler, sender, serverInstance) -> {
ServerPlayer player = handler.getPlayer();
this.applyTeamForPlayer(serverInstance, player, true);
teamRecheck.put(player.getUUID(), 20);
});
ServerTickEvents.END_SERVER_TICK.register(this::tick);
}
// --- Locator bar --------------------------------------------------------
/** Called by the locator bar mixin: may {@code receiver} see {@code source} on their locator bar? */
public static boolean locatorVisible(ServerPlayer source, ServerPlayer receiver) {
return switch (config.locatorBarMode()) {
case EVERYONE -> true;
case NOBODY -> false;
case CLAN -> {
String a = playerClans.get(source.getUUID());
yield a != null && a.equals(playerClans.get(receiver.getUUID()));
}
};
}
/** Rebuilds every player's locator bar connections after clan membership or the mode changes. */
private static void refreshLocatorBars(MinecraftServer server) {
for (ServerLevel level : server.getAllLevels()) {
for (ServerPlayer p : level.players()) {
level.getWaypointManager().remakeConnections(p);
}
}
}
private void tick(MinecraftServer server) {
if (!teamRecheck.isEmpty()) {
for (UUID id : new ArrayList<>(teamRecheck.keySet())) {
int left = teamRecheck.get(id) - 1;
if (left > 0) {
teamRecheck.put(id, left);
continue;
}
teamRecheck.remove(id);
ServerPlayer player = server.getPlayerList().getPlayer(id);
if (player != null) {
this.applyTeamForPlayer(server, player, false);
}
}
}
// Save every 10 seconds (only files that changed), so clans survive a
// server that is killed instead of stopped cleanly.
if (dataFolder != null && --ticksUntilSave <= 0) {
ticksUntilSave = 200;
this.saveAllData(false);
}
}
// --- Persistence --------------------------------------------------------
private void saveAllData() {
this.saveAllData(true);
}
private void writeIfChanged(Path file, String json) throws java.io.IOException {
String key = file.toString();
if (json.equals(lastWritten.get(key)) && Files.exists(file)) {
return;
}
Files.writeString(file, json);
lastWritten.put(key, json);
}
private void saveAllData(boolean log) {
try {
if (!Files.exists(dataFolder)) {
Files.createDirectories(dataFolder);
}
Path clansFile = dataFolder.resolve("clans.json");
ClanDataContainer clanContainer = new ClanDataContainer();
clanContainer.clans = new ArrayList<>(clans.values());
this.writeIfChanged(clansFile, gson.toJson(clanContainer));
Path invitesFile = dataFolder.resolve("invites.json");
InviteDataContainer inviteContainer = new InviteDataContainer();
inviteContainer.invites = new ArrayList<>();
for (Entry<UUID, UUID> entry : pendingInvites.entrySet()) {
inviteContainer.invites.add(new InviteEntry(entry.getKey(), entry.getValue(),
pendingInviteClans.get(entry.getKey())));
}
this.writeIfChanged(invitesFile, gson.toJson(inviteContainer));
Path requestsFile = dataFolder.resolve("requests.json");
RequestDataContainer requestContainer = new RequestDataContainer();
requestContainer.requests = new ArrayList<>();
for (Entry<String, List<UUID>> entry : joinRequests.entrySet()) {
requestContainer.requests.add(new RequestEntry(entry.getKey(), entry.getValue()));
}
this.writeIfChanged(requestsFile, gson.toJson(requestContainer));
if (log) {
System.out.println("[SimpleClan] Data saved successfully.");
}
} catch (Exception e) {
System.err.println("[SimpleClan] Failed to save data: " + e.getMessage());
e.printStackTrace();
}
}
private void loadAllData() {
try {
if (!Files.exists(dataFolder)) {
System.out.println("[SimpleClan] No data folder found, starting fresh.");
return;
}
Path clansFile = dataFolder.resolve("clans.json");
if (Files.exists(clansFile)) {
ClanDataContainer clanContainer = gson.fromJson(Files.readString(clansFile), ClanDataContainer.class);
if (clanContainer != null && clanContainer.clans != null) {
clans.clear();
for (Clan clan : clanContainer.clans) {
clans.put(clan.name.toLowerCase(Locale.ROOT), clan);
}
System.out.println("[SimpleClan] Loaded " + clans.size() + " clans.");
}
}
Path invitesFile = dataFolder.resolve("invites.json");
if (Files.exists(invitesFile)) {
InviteDataContainer inviteContainer = gson.fromJson(Files.readString(invitesFile), InviteDataContainer.class);
if (inviteContainer != null && inviteContainer.invites != null) {
pendingInvites.clear();
pendingInviteClans.clear();
for (InviteEntry entry : inviteContainer.invites) {
pendingInvites.put(entry.invitee, entry.inviter);
if (entry.clan != null) {
pendingInviteClans.put(entry.invitee, entry.clan);
}
}
}
}
Path requestsFile = dataFolder.resolve("requests.json");
if (Files.exists(requestsFile)) {
RequestDataContainer requestContainer = gson.fromJson(Files.readString(requestsFile), RequestDataContainer.class);
if (requestContainer != null && requestContainer.requests != null) {
joinRequests.clear();
for (RequestEntry entry : requestContainer.requests) {
joinRequests.put(entry.clanName, entry.requests);
}
}
}
} catch (Exception e) {
System.err.println("[SimpleClan] Failed to load data: " + e.getMessage());
e.printStackTrace();
}
}
private void rebuildPlayerClansMap() {
playerClans.clear();
for (Entry<String, Clan> entry : clans.entrySet()) {
for (UUID memberId : entry.getValue().members) {
playerClans.put(memberId, entry.getKey());
}
}
System.out.println("[SimpleClan] Rebuilt player-clan mapping for " + playerClans.size() + " players.");
}
// --- Commands -----------------------------------------------------------
private void registerCommands(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(literal("clan")
.then(literal("create")
.then(argument("name", StringArgumentType.word())
.then(argument("color", StringArgumentType.word()).executes(this::cmdCreate))))
.then(literal("invite")
.then(argument("player", StringArgumentType.word()).executes(this::cmdInvite)))
.then(literal("accept").executes(this::cmdAccept)
.then(argument("player", StringArgumentType.word()).executes(this::cmdAcceptPlayer)))
.then(literal("deny").executes(this::cmdDeny)
.then(argument("player", StringArgumentType.word()).executes(this::cmdDenyPlayer)))
.then(literal("kick")
.then(argument("player", StringArgumentType.word()).executes(this::cmdKick)))
.then(literal("leave").executes(this::cmdLeave))
.then(literal("disband").executes(this::cmdDisband))
.then(literal("transfer")
.then(argument("player", StringArgumentType.word()).executes(this::cmdTransfer)))
.then(literal("join")
.then(argument("name", StringArgumentType.word()).executes(this::cmdJoin)))
.then(literal("message")
.then(argument("message", StringArgumentType.greedyString()).executes(this::cmdMessage)))
.then(literal("list").executes(this::cmdList)
.then(literal("all").executes(this::cmdListAll)))
.then(literal("refresh").executes(this::cmdRefresh))
.then(literal("locatorbar")
.requires(Commands.hasPermission(Commands.LEVEL_OWNERS))
.executes(this::cmdLocatorBarShow)
.then(literal("everyone").executes(ctx -> cmdLocatorBarSet(ctx, ClanConfig.LocatorBarMode.EVERYONE)))
.then(literal("clan").executes(ctx -> cmdLocatorBarSet(ctx, ClanConfig.LocatorBarMode.CLAN)))
.then(literal("nobody").executes(ctx -> cmdLocatorBarSet(ctx, ClanConfig.LocatorBarMode.NOBODY))))
.then(literal("tagposition")
.requires(Commands.hasPermission(Commands.LEVEL_OWNERS))
.executes(this::cmdTagPositionShow)
.then(literal("left").executes(ctx -> cmdTagPositionSet(ctx, "left")))
.then(literal("right").executes(ctx -> cmdTagPositionSet(ctx, "right"))))
.then(literal("op")
.requires(Commands.hasPermission(Commands.LEVEL_OWNERS))
.then(argument("clanname", StringArgumentType.word())
.then(literal("disband").executes(this::cmdOpDisband))
.then(literal("invite")
.then(argument("player", StringArgumentType.word()).executes(this::cmdOpInvite)))
.then(literal("kick")
.then(argument("player", StringArgumentType.word()).executes(this::cmdOpKick)))
.then(literal("transfer")
.then(argument("player", StringArgumentType.word()).executes(this::cmdOpTransfer))))));
}
private static void msg(ServerPlayer player, Component text) {
player.sendSystemMessage(text);
}
private static Component red(String s) {
return Component.literal(s).withStyle(ChatFormatting.RED);
}
private static Component green(String s) {
return Component.literal(s).withStyle(ChatFormatting.GREEN);
}
private int cmdTagPositionShow(CommandContext<CommandSourceStack> ctx) {
ctx.getSource().sendSuccess(() -> Component.literal("Clan tags are shown "
+ (config.tagOnRight() ? "right of" : "left of") + " player names.")
.withStyle(ChatFormatting.AQUA), false);
return 1;
}
private int cmdTagPositionSet(CommandContext<CommandSourceStack> ctx, String side) {
config.tagPosition = side;
config.save();
this.restoreAllTeams(ctx.getSource().getServer());
ctx.getSource().sendSuccess(() -> Component.literal("Clan tags are now shown " + side + " of player names.")
.withStyle(ChatFormatting.GREEN), true);
return 1;
}
private int cmdLocatorBarShow(CommandContext<CommandSourceStack> ctx) {
ctx.getSource().sendSuccess(() -> Component.literal("Locator bar shows: " + config.locatorBarMode().label())
.withStyle(ChatFormatting.AQUA), false);
return 1;
}
private int cmdLocatorBarSet(CommandContext<CommandSourceStack> ctx, ClanConfig.LocatorBarMode mode) {
config.locatorBar = mode.name().toLowerCase(Locale.ROOT);
config.save();
refreshLocatorBars(ctx.getSource().getServer());
ctx.getSource().sendSuccess(() -> Component.literal("Locator bar now shows: " + mode.label())
.withStyle(ChatFormatting.GREEN), true);
return 1;
}
private int cmdCreate(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
UUID playerId = player.getUUID();
String name = StringArgumentType.getString(ctx, "name");
String colorArg = StringArgumentType.getString(ctx, "color").toUpperCase(Locale.ROOT);
if (playerClans.containsKey(playerId)) {
msg(player, red("You are already in a clan."));
return 1;
}
if (name.length() > 16) {
msg(player, red("Clan name too long (max 16 chars)."));
return 1;
}
ChatFormatting color;
try {
color = ChatFormatting.valueOf(colorArg);
} catch (Exception ex) {
msg(player, red("Invalid color. Example: RED, BLUE, GREEN"));
return 1;
}
String key = name.toLowerCase(Locale.ROOT);
if (clans.containsKey(key)) {
msg(player, red("A clan with that name already exists."));
return 1;
}
Clan clan = new Clan(name, color, playerId);
clan.members.add(playerId);
clans.put(key, clan);
playerClans.put(playerId, key);
this.applyTeamForClan(player.level().getServer(), clan);
refreshLocatorBars(player.level().getServer());
msg(player, Component.literal("Clan created: ").append(Component.literal(name).withStyle(color)));
return 1;
}
private int cmdInvite(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
UUID playerId = player.getUUID();
String targetName = StringArgumentType.getString(ctx, "player");
String clanKey = playerClans.get(playerId);
if (clanKey == null) {
msg(player, red("You are not in a clan."));
return 1;
}
Clan clan = clans.get(clanKey);
if (!clan.owner.equals(playerId)) {
msg(player, red("Only the owner can invite players."));
return 1;
}
ServerPlayer target = this.lookupOnlinePlayer(player.level().getServer(), targetName);
if (target == null) {
msg(player, red("Player not found."));
return 1;
}
if (playerClans.containsKey(target.getUUID())) {
msg(player, red("That player is already in a clan."));
return 1;
}
pendingInvites.put(target.getUUID(), playerId);
pendingInviteClans.put(target.getUUID(), clanKey);
msg(target, Component.literal(player.getName().getString() + " invited you to join clan ")
.append(Component.literal(clan.name).withStyle(clan.color))
.append(Component.literal(". Type /clan accept to join or /clan deny to decline."))
.withStyle(ChatFormatting.YELLOW));
msg(player, green("Invite sent to " + target.getName().getString()));
return 1;
}
private int cmdAccept(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
UUID playerId = player.getUUID();
if (!pendingInvites.containsKey(playerId)) {
msg(player, red("You have no pending invites."));
return 1;
}
UUID inviterId = pendingInvites.remove(playerId);
String inviteClan = pendingInviteClans.remove(playerId);
// Invites saved by older versions have no clan key; fall back to the inviter's clan.
String clanKey = inviteClan != null ? inviteClan : playerClans.get(inviterId);
if (clanKey == null || !clans.containsKey(clanKey)) {
msg(player, red("The inviting clan no longer exists."));
return 1;
}
if (playerClans.containsKey(playerId)) {
msg(player, red("You are already in a clan."));
return 1;
}
Clan clan = clans.get(clanKey);
clan.members.add(playerId);
playerClans.put(playerId, clanKey);
MinecraftServer server = player.level().getServer();
this.applyTeamForClan(server, clan);
refreshLocatorBars(server);
msg(player, Component.literal("You joined clan ").append(Component.literal(clan.name).withStyle(clan.color)));
ServerPlayer inviter = server.getPlayerList().getPlayer(inviterId);
if (inviter != null) {
msg(inviter, green(player.getName().getString() + " joined your clan."));
}
return 1;
}
private int cmdAcceptPlayer(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
UUID playerId = player.getUUID();
String targetName = StringArgumentType.getString(ctx, "player");
String ownerClanKey = playerClans.get(playerId);
if (ownerClanKey == null) {
msg(player, red("You are not in a clan."));
return 1;
}
Clan clan = clans.get(ownerClanKey);
if (!clan.owner.equals(playerId)) {
msg(player, red("Only the owner can accept join requests."));
return 1;
}
MinecraftServer server = player.level().getServer();
List<UUID> requests = joinRequests.getOrDefault(ownerClanKey, new ArrayList<>());
UUID found = findRequest(server, requests, targetName);
if (found == null) {
msg(player, red("No join request from that player (or they are offline)."));
return 1;
}
requests.remove(found);
joinRequests.put(ownerClanKey, requests);
if (playerClans.containsKey(found)) {
msg(player, red("That player is already in a clan."));
return 1;
}
clan.members.add(found);
playerClans.put(found, ownerClanKey);
this.applyTeamForClan(server, clan);
refreshLocatorBars(server);
ServerPlayer newly = server.getPlayerList().getPlayer(found);
if (newly != null) {
msg(newly, Component.literal("Your request to join clan ")
.append(Component.literal(clan.name).withStyle(clan.color))
.append(Component.literal(" was accepted."))
.withStyle(ChatFormatting.GREEN));
}
msg(player, green("Player accepted into the clan."));
return 1;
}
private int cmdDeny(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
UUID playerId = player.getUUID();
if (pendingInvites.remove(playerId) != null) {
pendingInviteClans.remove(playerId);
msg(player, Component.literal("Invite declined.").withStyle(ChatFormatting.YELLOW));
} else {
msg(player, red("You have no pending invites."));
}
return 1;
}
private int cmdDenyPlayer(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
UUID playerId = player.getUUID();
String targetName = StringArgumentType.getString(ctx, "player");
String ownerClanKey = playerClans.get(playerId);
if (ownerClanKey == null) {
msg(player, red("You are not in a clan."));
return 1;
}
Clan clan = clans.get(ownerClanKey);
if (!clan.owner.equals(playerId)) {
msg(player, red("Only the owner can deny join requests."));
return 1;
}
MinecraftServer server = player.level().getServer();
List<UUID> requests = joinRequests.getOrDefault(ownerClanKey, new ArrayList<>());
UUID found = findRequest(server, requests, targetName);
if (found == null) {
msg(player, red("No join request from that player (or they are offline)."));
return 1;
}
requests.remove(found);
joinRequests.put(ownerClanKey, requests);
ServerPlayer denied = server.getPlayerList().getPlayer(found);
if (denied != null) {
msg(denied, Component.literal("Your request to join ")
.append(Component.literal(clan.name).withStyle(clan.color))
.append(Component.literal(" was denied."))
.withStyle(ChatFormatting.YELLOW));
}
msg(player, green("Join request denied."));
return 1;
}
private static UUID findRequest(MinecraftServer server, List<UUID> requests, String targetName) {
for (UUID u : new ArrayList<>(requests)) {
ServerPlayer possible = server.getPlayerList().getPlayer(u);
if (possible != null && possible.getName().getString().equalsIgnoreCase(targetName)) {
return u;
}
}
return null;
}
private int cmdKick(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
UUID playerId = player.getUUID();
String targetName = StringArgumentType.getString(ctx, "player");
String clanKey = playerClans.get(playerId);
if (clanKey == null) {
msg(player, red("You are not in a clan."));
return 1;
}
Clan clan = clans.get(clanKey);
if (!clan.owner.equals(playerId)) {
msg(player, red("Only the owner can kick members."));
return 1;
}
MinecraftServer server = player.level().getServer();
ServerPlayer target = this.lookupOnlinePlayer(server, targetName);
if (target == null) {
msg(player, red("Player not found."));
return 1;
}
if (!clan.members.contains(target.getUUID())) {
msg(player, red("That player is not in your clan."));
return 1;
}
if (target.getUUID().equals(clan.owner)) {
msg(player, red("You can't kick yourself. Use /clan disband or /clan transfer."));
return 1;
}
clan.members.remove(target.getUUID());
playerClans.remove(target.getUUID());
this.removeFromTeam(server, target);
refreshLocatorBars(server);
msg(player, green(target.getName().getString() + " kicked from clan."));
msg(target, Component.literal("You were kicked from clan ")
.append(Component.literal(clan.name).withStyle(clan.color))
.withStyle(ChatFormatting.RED));
return 1;
}
private int cmdLeave(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
UUID playerId = player.getUUID();
String clanKey = playerClans.get(playerId);
if (clanKey == null) {
msg(player, red("You are not in a clan."));
return 1;
}
Clan clan = clans.get(clanKey);
if (clan.owner.equals(playerId)) {
msg(player, red("You are the owner. Use /clan disband or /clan transfer."));
return 1;
}
MinecraftServer server = player.level().getServer();
clan.members.remove(playerId);
playerClans.remove(playerId);
this.removeFromTeam(server, player);
refreshLocatorBars(server);
msg(player, Component.literal("You left the clan ")
.append(Component.literal(clan.name).withStyle(clan.color))
.withStyle(ChatFormatting.GREEN));
return 1;
}
private int cmdDisband(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
UUID playerId = player.getUUID();
String clanKey = playerClans.get(playerId);
if (clanKey == null) {
msg(player, red("You are not in a clan."));
return 1;
}
Clan clan = clans.get(clanKey);
if (!clan.owner.equals(playerId)) {
msg(player, red("Only the owner can disband."));
return 1;
}
disband(player.level().getServer(), clanKey, clan, "Your clan was disbanded.");
msg(player, green("Clan disbanded."));
return 1;
}
private void disband(MinecraftServer server, String clanKey, Clan clan, String notice) {
for (UUID m : new ArrayList<>(clan.members)) {
ServerPlayer p = server.getPlayerList().getPlayer(m);
if (p != null) {
msg(p, red(notice));
this.removeFromTeam(server, p);
}
playerClans.remove(m);
}
clans.remove(clanKey);
joinRequests.remove(clanKey);
// Drop the whole team, which also clears the tag of offline members.
PlayerTeam team = server.getScoreboard().getPlayerTeam(this.teamIdFromClanName(clan.name));
if (team != null) {
server.getScoreboard().removePlayerTeam(team);
}
refreshLocatorBars(server);
}
private int cmdTransfer(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
UUID playerId = player.getUUID();
String targetName = StringArgumentType.getString(ctx, "player");
String clanKey = playerClans.get(playerId);
if (clanKey == null) {
msg(player, red("You are not in a clan."));
return 1;
}
Clan clan = clans.get(clanKey);
if (!clan.owner.equals(playerId)) {
msg(player, red("Only the owner can transfer ownership."));
return 1;
}
MinecraftServer server = player.level().getServer();
ServerPlayer newOwner = this.lookupOnlinePlayer(server, targetName);
if (newOwner != null && clan.members.contains(newOwner.getUUID())) {
clan.owner = newOwner.getUUID();
msg(player, green("Ownership transferred to " + newOwner.getName().getString()));
msg(newOwner, green("You are now the clan owner."));
this.applyTeamForClan(server, clan);
} else {
msg(player, red("Player not found or not in your clan."));
}
return 1;
}
private int cmdJoin(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
UUID playerId = player.getUUID();
String targetClanName = StringArgumentType.getString(ctx, "name").toLowerCase(Locale.ROOT);
if (!clans.containsKey(targetClanName)) {
msg(player, red("Clan not found."));
return 1;
}
if (playerClans.containsKey(playerId)) {
msg(player, red("You are already in a clan."));
return 1;
}
List<UUID> reqs = joinRequests.computeIfAbsent(targetClanName, k -> new ArrayList<>());
if (!reqs.contains(playerId)) {
reqs.add(playerId);
}
Clan requestClan = clans.get(targetClanName);
ServerPlayer owner = player.level().getServer().getPlayerList().getPlayer(requestClan.owner);
String name = player.getName().getString();
if (owner != null) {
msg(owner, Component.literal(name + " requested to join your clan. Type /clan accept " + name
+ " or /clan deny " + name).withStyle(ChatFormatting.YELLOW));
}
msg(player, Component.literal("Join request sent to clan ")
.append(Component.literal(requestClan.name).withStyle(requestClan.color))
.withStyle(ChatFormatting.GREEN));
return 1;
}
private int cmdMessage(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
String message = StringArgumentType.getString(ctx, "message");
String clanKey = playerClans.get(player.getUUID());
if (clanKey == null) {
msg(player, red("You are not in a clan."));
return 1;
}
Clan clan = clans.get(clanKey);
for (UUID memberId : clan.members) {
ServerPlayer member = player.level().getServer().getPlayerList().getPlayer(memberId);
if (member != null) {
Component tag = Component.literal("[" + clan.name + "]").withStyle(clan.color);
String name = player.getName().getString();
msg(member, config.tagOnRight()
? Component.literal(name + " ").withStyle(ChatFormatting.WHITE).append(tag)
.append(Component.literal(": " + message).withStyle(ChatFormatting.WHITE))
: Component.empty().append(tag)
.append(Component.literal(" " + name + ": " + message).withStyle(ChatFormatting.WHITE)));
}
}
return 1;
}
private int cmdList(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
String clanKey = playerClans.get(player.getUUID());
if (clanKey == null) {
msg(player, red("You are not in a clan."));
return 1;
}
Clan clan = clans.get(clanKey);
msg(player, Component.literal("Clan ")
.append(Component.literal(clan.name).withStyle(clan.color))
.append(Component.literal(" members:"))
.withStyle(ChatFormatting.AQUA));
for (UUID m : clan.members) {
msg(player, Component.literal(this.getNameIfOnline(player.level().getServer(), m)));
}
return 1;
}
private int cmdListAll(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
if (clans.isEmpty()) {
msg(player, Component.literal("No clans exist.").withStyle(ChatFormatting.YELLOW));
return 1;
}
MinecraftServer server = player.level().getServer();
msg(player, Component.literal("All clans:").withStyle(ChatFormatting.AQUA));
for (Clan c : clans.values()) {
String members = c.members.stream().map(uuid -> this.getNameIfOnline(server, uuid))
.collect(Collectors.joining(", "));
msg(player, Component.literal(c.name + ":\n").withStyle(c.color)
.append(Component.literal("owner: " + this.getNameIfOnline(server, c.owner)
+ "\nmembers: " + members).withStyle(ChatFormatting.WHITE)));
}
return 1;
}
private int cmdRefresh(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
String clanKey = playerClans.get(player.getUUID());
if (clanKey == null) {
msg(player, red("You are not in a clan."));
return 1;
}
Clan clan = clans.get(clanKey);
this.applyTeamForClan(player.level().getServer(), clan);
refreshLocatorBars(player.level().getServer());
msg(player, green("Clan prefix refreshed!"));
System.out.println("[SimpleClan] Manual refresh for " + player.getName().getString() + " in clan " + clan.name);
return 1;
}
private int cmdOpDisband(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
String clanName = StringArgumentType.getString(ctx, "clanname").toLowerCase(Locale.ROOT);
Clan clan = clans.get(clanName);
if (clan == null) {
msg(player, red("Clan not found."));
return 1;
}
disband(player.level().getServer(), clanName, clan, "Your clan was disbanded by an operator.");
msg(player, green("Clan " + clan.name + " disbanded by operator."));
return 1;
}
private int cmdOpInvite(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
String clanName = StringArgumentType.getString(ctx, "clanname").toLowerCase(Locale.ROOT);
String targetName = StringArgumentType.getString(ctx, "player");
Clan clan = clans.get(clanName);
if (clan == null) {
msg(player, red("Clan not found."));
return 1;
}
ServerPlayer target = this.lookupOnlinePlayer(player.level().getServer(), targetName);
if (target == null) {
msg(player, red("Player not found."));
return 1;
}
if (playerClans.containsKey(target.getUUID())) {
msg(player, red("That player is already in a clan."));
return 1;
}
pendingInvites.put(target.getUUID(), player.getUUID());
pendingInviteClans.put(target.getUUID(), clanName);
msg(target, Component.literal("An operator invited you to join clan ")
.append(Component.literal(clan.name).withStyle(clan.color))
.append(Component.literal(". Type /clan accept to join or /clan deny to decline."))
.withStyle(ChatFormatting.YELLOW));
msg(player, green("Operator invite sent to " + target.getName().getString()));
return 1;
}
private int cmdOpKick(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
String clanName = StringArgumentType.getString(ctx, "clanname").toLowerCase(Locale.ROOT);
String targetName = StringArgumentType.getString(ctx, "player");
Clan clan = clans.get(clanName);
if (clan == null) {
msg(player, red("Clan not found."));
return 1;
}
MinecraftServer server = player.level().getServer();
ServerPlayer target = this.lookupOnlinePlayer(server, targetName);
if (target == null) {
msg(player, red("Player not found."));
return 1;
}
if (!clan.members.contains(target.getUUID())) {
msg(player, red("That player is not in the clan."));
return 1;
}
if (target.getUUID().equals(clan.owner)) {
msg(player, red("That player owns the clan. Transfer ownership or disband the clan instead."));
return 1;
}
clan.members.remove(target.getUUID());
playerClans.remove(target.getUUID());
this.removeFromTeam(server, target);
refreshLocatorBars(server);
msg(player, green(target.getName().getString() + " kicked from clan " + clan.name + " by operator."));
msg(target, Component.literal("You were kicked from clan ")
.append(Component.literal(clan.name).withStyle(clan.color))
.append(Component.literal(" by an operator."))
.withStyle(ChatFormatting.RED));
return 1;
}
private int cmdOpTransfer(CommandContext<CommandSourceStack> ctx) {
ServerPlayer player = this.getPlayer(ctx);
if (player == null) {
return 0;
}
String clanName = StringArgumentType.getString(ctx, "clanname").toLowerCase(Locale.ROOT);
String targetName = StringArgumentType.getString(ctx, "player");
Clan clan = clans.get(clanName);
if (clan == null) {
msg(player, red("Clan not found."));
return 1;
}
MinecraftServer server = player.level().getServer();
ServerPlayer newOwner = this.lookupOnlinePlayer(server, targetName);
if (newOwner != null && clan.members.contains(newOwner.getUUID())) {
clan.owner = newOwner.getUUID();
msg(player, green("Ownership of clan " + clan.name + " transferred to "
+ newOwner.getName().getString() + " by operator."));
msg(newOwner, green("You are now the clan owner (transferred by operator)."));
this.applyTeamForClan(server, clan);
} else {
msg(player, red("Player not found or not in the clan."));
}
return 1;
}
// --- Scoreboard teams ---------------------------------------------------
private void restoreAllTeams(MinecraftServer server) {
System.out.println("[SimpleClan] Restoring teams for " + clans.size() + " clans");
for (Clan clan : clans.values()) {
this.applyTeamForClan(server, clan);
}
}
private void applyTeamForClan(MinecraftServer server, Clan clan) {
Scoreboard sb = server.getScoreboard();
String teamId = this.teamIdFromClanName(clan.name);
PlayerTeam team = sb.getPlayerTeam(teamId);
if (team == null) {
team = sb.addPlayerTeam(teamId);
}
if (config.tagOnRight()) {
team.setPlayerPrefix(Component.empty());
team.setPlayerSuffix(Component.literal(" [" + clan.name + "]").withStyle(clan.color));
} else {
team.setPlayerPrefix(Component.literal("[" + clan.name + "] ").withStyle(clan.color));
team.setPlayerSuffix(Component.empty());
}
// Only remove online players who are no longer members. Offline members
// stay in the team, so their tag is already there when they rejoin.
for (ServerPlayer online : server.getPlayerList().getPlayers()) {
String name = online.getScoreboardName();
if (team.getPlayers().contains(name) && !clan.members.contains(online.getUUID())) {
sb.removePlayerFromTeam(name, team);
}
}
for (UUID memberId : clan.members) {
ServerPlayer player = server.getPlayerList().getPlayer(memberId);
if (player != null && team != sb.getPlayersTeam(player.getScoreboardName())) {
sb.addPlayerToTeam(player.getScoreboardName(), team);
}
}
}
/** Puts a player into their clan's team (or out of all clan teams), without needing the player list. */
private void applyTeamForPlayer(MinecraftServer server, ServerPlayer player, boolean log) {
String clanKey = playerClans.get(player.getUUID());
Clan clan = clanKey != null ? clans.get(clanKey) : null;
String name = player.getScoreboardName();
Scoreboard sb = server.getScoreboard();
if (clan == null) {
PlayerTeam current = sb.getPlayersTeam(name);
if (current != null && current.getName().startsWith("clan_")) {
sb.removePlayerFromTeam(name, current);
}
return;
}
this.applyTeamForClan(server, clan);
PlayerTeam team = sb.getPlayerTeam(this.teamIdFromClanName(clan.name));
if (team != null && sb.getPlayersTeam(name) != team) {
sb.addPlayerToTeam(name, team);
}
if (log) {
System.out.println("[SimpleClan] Applied team for " + name + " in clan " + clan.name);
}
}
private void removeFromTeam(MinecraftServer server, ServerPlayer player) {
Scoreboard sb = server.getScoreboard();
String playerName = player.getScoreboardName();
for (PlayerTeam t : new ArrayList<>(sb.getPlayerTeams())) {
if (t.getPlayers().contains(playerName)) {
sb.removePlayerFromTeam(playerName, t);
}
}
}
private String teamIdFromClanName(String clanName) {
String id = "clan_" + clanName.replaceAll("[^A-Za-z0-9]", "");
return id.length() > 16 ? id.substring(0, 16) : id;
}
private String getNameIfOnline(MinecraftServer server, UUID uuid) {
ServerPlayer p = server.getPlayerList().getPlayer(uuid);
return p != null ? p.getName().getString() : uuid.toString();
}
private ServerPlayer getPlayer(CommandContext<CommandSourceStack> ctx) {
try {
return ctx.getSource().getPlayerOrException();
} catch (Exception e) {
ctx.getSource().sendSuccess(() -> red("Only players can use this command."), false);
return null;
}
}
private ServerPlayer lookupOnlinePlayer(MinecraftServer server, String name) {
for (ServerPlayer p : server.getPlayerList().getPlayers()) {
if (p.getName().getString().equalsIgnoreCase(name)) {
return p;
}
}
return null;
}
// --- Data ---------------------------------------------------------------
private static class Clan {
String name;
ChatFormatting color;
UUID owner;
Set<UUID> members = new HashSet<>();
Clan(String name, ChatFormatting color, UUID owner) {
this.name = name;
this.color = color;
this.owner = owner;
}
}
private static class ClanDataContainer {
List<Clan> clans;
}
private static class InviteDataContainer {
List<InviteEntry> invites;
}
private static class InviteEntry {
UUID invitee;
UUID inviter;
/** Added in 1.1.0; null in files written by older versions. */
String clan;
InviteEntry(UUID invitee, UUID inviter, String clan) {
this.invitee = invitee;
this.inviter = inviter;
this.clan = clan;
}
}
private static class RequestDataContainer {
List<RequestEntry> requests;
}
private static class RequestEntry {
String clanName;
List<UUID> requests;
RequestEntry(String clanName, List<UUID> requests) {
this.clanName = clanName;
this.requests = requests;
}
}
}
ClanConfig.java
src/main/java/com/simpleclan/ClanConfig.java
package com.simpleclan;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.fabricmc.loader.api.FabricLoader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
/** JSON config stored at config/simple-clan.json. */
public final class ClanConfig {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
public enum LocatorBarMode {
EVERYONE("everyone"), CLAN("only your own clan"), NOBODY("nobody");
private final String label;
LocatorBarMode(String label) {
this.label = label;
}
public String label() {
return label;
}
}
/** Who shows up on a player's locator bar: "everyone", "clan" or "nobody". */
public String locatorBar = "everyone";
/** Where the [Clan] tag goes: "left" ([Clan] Steve) or "right" (Steve [Clan]). */
public String tagPosition = "left";
public boolean tagOnRight() {
return tagPosition != null && tagPosition.trim().equalsIgnoreCase("right");
}
public LocatorBarMode locatorBarMode() {
try {
return LocatorBarMode.valueOf(locatorBar.trim().toUpperCase(Locale.ROOT));
} catch (RuntimeException e) {
return LocatorBarMode.EVERYONE;
}
}
private static Path path() {
return FabricLoader.getInstance().getConfigDir().resolve("simple-clan.json");
}
public static ClanConfig load() {
Path path = path();
try {
if (Files.exists(path)) {
ClanConfig c = GSON.fromJson(Files.readString(path), ClanConfig.class);
if (c != null) {
if (c.tagPosition == null) {
c.tagPosition = "left";
}
c.save(); // adds options that older config files don't have yet
return c;
}
}
ClanConfig fresh = new ClanConfig();
fresh.save();
return fresh;
} catch (IOException | RuntimeException e) {
return new ClanConfig();
}
}
public void save() {
try {
Files.createDirectories(path().getParent());
Files.writeString(path(), GSON.toJson(this));
} catch (IOException ignored) {
}
}
}
LocatorBarMixin.java
src/main/java/com/simpleclan/mixin/LocatorBarMixin.java
package com.simpleclan.mixin;
import com.simpleclan.SimpleClan;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.waypoints.WaypointTransmitter;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Optional;
/**
* Players are locator bar "transmitters". Refusing the connection here means the
* receiving player's client never gets that player's dot, so this works with
* vanilla clients.
*/
@Mixin(LivingEntity.class)
public abstract class LocatorBarMixin {
@Inject(method = "makeWaypointConnectionWith", at = @At("HEAD"), cancellable = true)
private void simpleclan$filterLocatorBar(ServerPlayer receiver,
CallbackInfoReturnable<Optional<WaypointTransmitter.Connection>> cir) {
if ((Object) this instanceof ServerPlayer source && source != receiver
&& !SimpleClan.locatorVisible(source, receiver)) {
cir.setReturnValue(Optional.empty());
}
}
}
simple-clan.mixins.json
src/main/resources/simple-clan.mixins.json
{
"required": true,
"package": "com.simpleclan.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": [
"LocatorBarMixin"
],
"injectors": {
"defaultRequire": 1
}
}
fabric.mod.json
src/main/resources/fabric.mod.json
{
"schemaVersion": 1,
"id": "simple-clan",
"version": "1.2.2",
"name": "SimpleClan",
"description": "Player clans with colored name prefixes, clan chat, and locator bar visibility control.",
"authors": ["Oakbanner"],
"license": "All-Rights-Reserved",
"icon": "assets/simple-clan/icon.png",
"environment": "*",
"entrypoints": {
"main": ["com.simpleclan.SimpleClan"]
},
"mixins": ["simple-clan.mixins.json"],
"depends": {
"fabricloader": ">=0.19.5",
"fabric-api": "*",
"minecraft": ">=26.1",
"java": ">=25"
}
}