package ar.com.companeros.horror; import java.util.*; import net.minecraft.core.BlockPos; import net.minecraft.nbt.*; import net.minecraft.server.MinecraftServer; import net.minecraft.world.level.saveddata.SavedData; /** Un solo acontecimiento, un solo huésped y un depósito sellado de sus pertenencias. */ public final class HorrorState extends SavedData { public static final int SCHEMA = 1; private long startTime, meteorTime = -1, infectionTime = -1; private BlockPos meteor; private UUID host, entityId; private String hostName = "", entityDimension = "minecraft:overworld"; private String appearanceOverride = ""; private BlockPos entityPosition = BlockPos.ZERO; private int minimumStage; private boolean transferred, entityCreated, dead; private CompoundTag quarantine = new CompoundTag(); private final Set sensitive = new HashSet<>(); public HorrorState(long startTime) { if (startTime < 0) throw new IllegalArgumentException("Tiempo inválido"); this.startTime = startTime; setDirty(); } public static HorrorState get(MinecraftServer server) { if (!server.isSameThread()) throw new IllegalStateException("El parásito usa el hilo del servidor"); return server.overworld().getDataStorage().computeIfAbsent(HorrorState::load, () -> new HorrorState(server.overworld().getGameTime()), "raps_parasite"); } public boolean meteorDue(long time, HorrorConfig config) { return meteor == null && time >= startTime && time - startTime >= config.meteorDelayTicks(); } public boolean infectionDue(long time, HorrorConfig config) { return host == null && meteor != null && time >= meteorTime && time - meteorTime >= config.infectionDelayTicks(); } public boolean meteorLanded() { return meteor != null; } public Optional meteor() { return Optional.ofNullable(meteor); } public Optional host() { return Optional.ofNullable(host); } public String hostName() { return hostName; } public String appearanceOverride() { return appearanceOverride; } public void appearanceOverride(String value) { if (!"".equals(value) && !"steve".equals(value)) throw new IllegalArgumentException("Aspecto del parásito inválido"); if (!appearanceOverride.equals(value)) { appearanceOverride = value; setDirty(); } } public boolean active() { return host != null; } public boolean transferred() { return transferred; } public boolean entityCreated() { return entityCreated; } public boolean dead() { return dead; } public Optional entityId() { return Optional.ofNullable(entityId); } public String entityDimension() { return entityDimension; } public BlockPos entityPosition() { return entityPosition; } public long infectionTime() { return infectionTime; } public CompoundTag quarantine() { return quarantine.copy(); } public boolean sensitive(UUID player) { return sensitive.contains(player); } public void sensitivity(UUID player, boolean enabled) { if (enabled ? sensitive.add(player) : sensitive.remove(player)) setDirty(); } // El reloj nunca depende del uptime ni de /time set; el avance administrativo sólo eleva el mínimo. public int stage(long time, HorrorConfig config) { if (!active()) return 0; long elapsed = Math.max(0, time - infectionTime); return Math.max(minimumStage, (int) Math.min(config.finalStage(), elapsed / config.stageDurationTicks())); } public void forceStage(int stage) { if (!active() || stage < minimumStage || stage > 4) throw new IllegalArgumentException("La mutación sólo avanza entre 0 y 4"); minimumStage = stage; setDirty(); } public void landed(BlockPos pos, long time) { if (meteor != null) throw new IllegalStateException("El meteorito ya cayó"); meteor = pos.immutable(); meteorTime = time; setDirty(); } public void selectHost(UUID id, String name, long time) { if (host != null) { if (host.equals(id)) return; throw new IllegalStateException("Ya existe un huésped"); } if (id == null || !name.matches("[A-Za-z0-9_]{1,16}")) throw new IllegalArgumentException("Huésped inválido"); host = id; hostName = name; infectionTime = time; entityId = UUID.randomUUID(); setDirty(); } public void prepareQuarantine(CompoundTag playerSnapshot, String dimension, BlockPos pos) { if (host == null || transferred) throw new IllegalStateException("No se puede iniciar la transferencia"); if (!quarantine.isEmpty()) return; quarantine = playerSnapshot.copy(); entityDimension = dimension; entityPosition = pos.immutable(); setDirty(); } public void commitTransfer() { if (quarantine.isEmpty()) throw new IllegalStateException("Falta el respaldo del inventario"); transferred = true; setDirty(); } public void created() { if (!transferred) throw new IllegalStateException("Inventario pendiente"); entityCreated = true; dead = false; setDirty(); } public void location(String dimension, BlockPos pos) { if (!entityDimension.equals(dimension) || !entityPosition.equals(pos)) { entityDimension = dimension; entityPosition = pos.immutable(); setDirty(); } } public void died() { if (!dead) { dead = true; setDirty(); } } @Override public CompoundTag save(CompoundTag tag) { tag.putInt("SchemaVersion", SCHEMA); tag.putLong("StartTime", startTime); tag.putString("AppearanceOverride", appearanceOverride); tag.putLong("MeteorTime", meteorTime); tag.putLong("InfectionTime", infectionTime); if (meteor != null) tag.putLong("Meteor", meteor.asLong()); if (host != null) { tag.putUUID("Host", host); tag.putString("HostName", hostName); } if (entityId != null) tag.putUUID("EntityId", entityId); tag.putString("EntityDimension", entityDimension); tag.putLong("EntityPosition", entityPosition.asLong()); tag.putInt("MinimumStage", minimumStage); tag.putBoolean("Transferred", transferred); tag.putBoolean("EntityCreated", entityCreated); tag.putBoolean("Dead", dead); tag.put("Quarantine", quarantine.copy()); ListTag players = new ListTag(); sensitive.stream().sorted().forEach(id -> players.add(NbtUtils.createUUID(id))); tag.put("Sensitive", players); return tag; } public static HorrorState load(CompoundTag tag) { if (tag.getInt("SchemaVersion") != SCHEMA) throw new IllegalStateException("Formato del parásito desconocido"); HorrorState state = new HorrorState(tag.getLong("StartTime")); state.appearanceOverride(tag.getString("AppearanceOverride")); state.meteorTime = tag.getLong("MeteorTime"); state.infectionTime = tag.getLong("InfectionTime"); state.meteor = tag.contains("Meteor", Tag.TAG_LONG) ? BlockPos.of(tag.getLong("Meteor")) : null; state.host = tag.hasUUID("Host") ? tag.getUUID("Host") : null; state.hostName = tag.getString("HostName"); state.entityId = tag.hasUUID("EntityId") ? tag.getUUID("EntityId") : null; state.entityDimension = tag.getString("EntityDimension"); state.entityPosition = BlockPos.of(tag.getLong("EntityPosition")); state.minimumStage = tag.getInt("MinimumStage"); state.transferred = tag.getBoolean("Transferred"); state.entityCreated = tag.getBoolean("EntityCreated"); state.dead = tag.getBoolean("Dead"); state.quarantine = tag.getCompound("Quarantine").copy(); if (state.minimumStage < 0 || state.minimumStage > 4 || state.quarantine.size() > 1024 || tag.getList("Sensitive", Tag.TAG_INT_ARRAY).size() > 4096 || (state.host != null && (!state.hostName.matches("[A-Za-z0-9_]{1,16}") || state.entityId == null)) || (state.transferred && (state.host == null || state.quarantine.isEmpty()))) throw new IllegalStateException("Estado del parásito inválido"); for (Tag id : tag.getList("Sensitive", Tag.TAG_INT_ARRAY)) state.sensitive.add(NbtUtils.loadUUID(id)); state.setDirty(false); return state; } }