package com.tiedup.remake.state; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.Tag; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.level.saveddata.SavedData; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Global registry for collar ownership relationships. * * This registry tracks which entities are wearing collars and who owns them. * It persists across server restarts and provides efficient lookups in both directions: * - Owner UUID → Set of collar-wearer UUIDs (slaves) * - Wearer UUID → Set of owner UUIDs (masters) * * Terminology: * - "Slave" = Entity wearing a collar owned by a player (passive ownership) * - "Captive" = Entity attached by leash (active physical control) - managed by PlayerCaptiveManager * */ public class CollarRegistry extends SavedData { private static final String DATA_NAME = "tiedup_collar_registry"; // Owner UUID → Set of wearer UUIDs (a master can own multiple slaves) private final Map> ownerToWearers = new ConcurrentHashMap<>(); // Wearer UUID → Set of owner UUIDs (a collar can have multiple owners) private final Map> wearerToOwners = new ConcurrentHashMap<>(); // ==================== STATIC ACCESS ==================== /** * Get the CollarRegistry for a server level. * Creates a new registry if one doesn't exist. */ public static CollarRegistry get(ServerLevel level) { return level .getDataStorage() .computeIfAbsent( CollarRegistry::load, CollarRegistry::new, DATA_NAME ); } /** * Get the CollarRegistry from a MinecraftServer. * Uses the overworld as the storage dimension. */ public static CollarRegistry get(MinecraftServer server) { ServerLevel overworld = server.overworld(); return get(overworld); } /** * Convenience method to get registry from a ServerPlayer. */ @Nullable public static CollarRegistry get(ServerPlayer player) { if (player.getServer() == null) return null; return get(player.getServer()); } // ==================== REGISTRATION ==================== /** * Register a collar relationship: owner now owns the collar on wearer. * * @param wearerUUID UUID of the entity wearing the collar * @param ownerUUID UUID of the collar's owner */ public void registerCollar(UUID wearerUUID, UUID ownerUUID) { // Add to owner → wearers map ownerToWearers .computeIfAbsent(ownerUUID, k -> ConcurrentHashMap.newKeySet()) .add(wearerUUID); // Add to wearer → owners map wearerToOwners .computeIfAbsent(wearerUUID, k -> ConcurrentHashMap.newKeySet()) .add(ownerUUID); setDirty(); } /** * Register a collar with multiple owners at once. * * @param wearerUUID UUID of the entity wearing the collar * @param ownerUUIDs Set of owner UUIDs */ public void registerCollar(UUID wearerUUID, Set ownerUUIDs) { for (UUID ownerUUID : ownerUUIDs) { registerCollar(wearerUUID, ownerUUID); } } /** * Unregister a specific owner from a collar wearer. * * @param wearerUUID UUID of the entity wearing the collar * @param ownerUUID UUID of the owner to remove */ private void unregisterOwner(UUID wearerUUID, UUID ownerUUID) { // Remove from owner → wearers map using atomic operation ownerToWearers.computeIfPresent(ownerUUID, (key, wearers) -> { wearers.remove(wearerUUID); return wearers.isEmpty() ? null : wearers; }); // Remove from wearer → owners map using atomic operation wearerToOwners.computeIfPresent(wearerUUID, (key, owners) -> { owners.remove(ownerUUID); return owners.isEmpty() ? null : owners; }); setDirty(); } /** * Completely unregister a collar wearer (removes all owner relationships). * Called when a collar is removed from an entity. * * @param wearerUUID UUID of the entity whose collar was removed */ public void unregisterWearer(UUID wearerUUID) { Set owners = wearerToOwners.remove(wearerUUID); if (owners != null) { for (UUID ownerUUID : owners) { // Use atomic operation for thread-safe removal ownerToWearers.computeIfPresent(ownerUUID, (key, wearers) -> { wearers.remove(wearerUUID); return wearers.isEmpty() ? null : wearers; }); } setDirty(); } } /** * Update a wearer's owners completely (replaces all existing owners). * Useful when collar NBT is the source of truth. * * @param wearerUUID UUID of the entity wearing the collar * @param newOwnerUUIDs New set of owner UUIDs */ public void updateWearerOwners(UUID wearerUUID, Set newOwnerUUIDs) { // Get current owners Set currentOwners = wearerToOwners.get(wearerUUID); if (currentOwners == null) { currentOwners = Collections.emptySet(); } // Find owners to remove Set toRemove = new HashSet<>(currentOwners); toRemove.removeAll(newOwnerUUIDs); // Find owners to add Set toAdd = new HashSet<>(newOwnerUUIDs); toAdd.removeAll(currentOwners); // Apply changes for (UUID ownerUUID : toRemove) { unregisterOwner(wearerUUID, ownerUUID); } for (UUID ownerUUID : toAdd) { registerCollar(wearerUUID, ownerUUID); } } // ==================== QUERIES ==================== /** * Get all slaves (collar wearers) owned by a specific owner. * * @param ownerUUID UUID of the owner * @return Unmodifiable set of wearer UUIDs (never null) */ public Set getSlaves(UUID ownerUUID) { Set wearers = ownerToWearers.get(ownerUUID); if (wearers == null) return Collections.emptySet(); return Collections.unmodifiableSet(new HashSet<>(wearers)); } /** * Get all owners of a specific collar wearer. * * @param wearerUUID UUID of the wearer * @return Unmodifiable set of owner UUIDs (never null) */ public Set getOwners(UUID wearerUUID) { Set owners = wearerToOwners.get(wearerUUID); if (owners == null) return Collections.emptySet(); return Collections.unmodifiableSet(new HashSet<>(owners)); } /** * Check if an owner has any slaves. */ public boolean hasSlaves(UUID ownerUUID) { Set wearers = ownerToWearers.get(ownerUUID); return wearers != null && !wearers.isEmpty(); } /** * Check if a wearer has any owners. */ public boolean hasOwners(UUID wearerUUID) { Set owners = wearerToOwners.get(wearerUUID); return owners != null && !owners.isEmpty(); } /** * Check if a specific owner owns a specific wearer. */ public boolean isOwner(UUID ownerUUID, UUID wearerUUID) { Set wearers = ownerToWearers.get(ownerUUID); return wearers != null && wearers.contains(wearerUUID); } /** * Get the count of slaves for an owner. */ public int getSlaveCount(UUID ownerUUID) { Set wearers = ownerToWearers.get(ownerUUID); return wearers == null ? 0 : wearers.size(); } /** * Get all registered wearers (for admin/debug purposes). */ public Set getAllWearers() { return Collections.unmodifiableSet( new HashSet<>(wearerToOwners.keySet()) ); } /** * Get all registered owners (for admin/debug purposes). */ public Set getAllOwners() { return Collections.unmodifiableSet( new HashSet<>(ownerToWearers.keySet()) ); } // ==================== ENTITY RESOLUTION ==================== /** * Find all slave entities for an owner that are currently loaded. * This is useful for GUI and proximity-based actions. * * @param owner The owner player * @return List of currently loaded slave entities */ public List findLoadedSlaves(ServerPlayer owner) { List loadedSlaves = new ArrayList<>(); Set slaveUUIDs = getSlaves(owner.getUUID()); MinecraftServer server = owner.getServer(); if (server == null) return loadedSlaves; for (UUID slaveUUID : slaveUUIDs) { Entity entity = findEntityByUUID(server, slaveUUID); if (entity instanceof LivingEntity living) { loadedSlaves.add(living); } } return loadedSlaves; } /** * Find an entity by UUID across all dimensions. * Optimized: checks player list first (O(1)) before searching dimensions. */ @Nullable private Entity findEntityByUUID(MinecraftServer server, UUID uuid) { // Check player first - O(1) lookup net.minecraft.server.level.ServerPlayer player = server .getPlayerList() .getPlayer(uuid); if (player != null) { return player; } // Fallback: search dimensions for NPCs for (ServerLevel level : server.getAllLevels()) { Entity entity = level.getEntity(uuid); if (entity != null) { return entity; } } return null; } // ==================== PERSISTENCE ==================== @Override public @NotNull CompoundTag save(@NotNull CompoundTag tag) { ListTag registryList = new ListTag(); for (Map.Entry> entry : wearerToOwners.entrySet()) { CompoundTag wearerTag = new CompoundTag(); wearerTag.putUUID("wearer", entry.getKey()); ListTag ownersTag = new ListTag(); for (UUID ownerUUID : entry.getValue()) { CompoundTag ownerTag = new CompoundTag(); ownerTag.putUUID("uuid", ownerUUID); ownersTag.add(ownerTag); } wearerTag.put("owners", ownersTag); registryList.add(wearerTag); } tag.put("collar_registry", registryList); return tag; } public static CollarRegistry load(CompoundTag tag) { CollarRegistry registry = new CollarRegistry(); ListTag registryList = tag.getList("collar_registry", Tag.TAG_COMPOUND); for (int i = 0; i < registryList.size(); i++) { CompoundTag wearerTag = registryList.getCompound(i); UUID wearerUUID = wearerTag.getUUID("wearer"); ListTag ownersTag = wearerTag.getList("owners", Tag.TAG_COMPOUND); for (int j = 0; j < ownersTag.size(); j++) { CompoundTag ownerTag = ownersTag.getCompound(j); UUID ownerUUID = ownerTag.getUUID("uuid"); // Register relationship (without marking dirty - we're loading) registry.ownerToWearers .computeIfAbsent(ownerUUID, k -> ConcurrentHashMap.newKeySet() ) .add(wearerUUID); registry.wearerToOwners .computeIfAbsent(wearerUUID, k -> ConcurrentHashMap.newKeySet() ) .add(ownerUUID); } } return registry; } // ==================== DEBUG ==================== /** * Get a debug string representation of the registry. */ public String toDebugString() { StringBuilder sb = new StringBuilder(); sb.append("CollarRegistry:\n"); sb.append(" Owners: ").append(ownerToWearers.size()).append("\n"); sb.append(" Wearers: ").append(wearerToOwners.size()).append("\n"); for (Map.Entry> entry : ownerToWearers.entrySet()) { sb .append(" Owner ") .append(entry.getKey().toString().substring(0, 8)) .append("... → ") .append(entry.getValue().size()) .append(" slaves\n"); } return sb.toString(); } }