package com.minelittlepony.unicopia; import java.util.*; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.spongepowered.include.com.google.common.base.Objects; import com.google.common.base.Strings; import com.google.common.base.Suppliers; import com.minelittlepony.unicopia.ability.magic.Affine; import com.minelittlepony.unicopia.util.RegistryUtils; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; import net.minecraft.command.argument.RegistryKeyArgumentType; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.Text; import net.minecraft.util.Identifier; import net.minecraft.registry.Registry; import net.minecraft.registry.RegistryKey; public record Race (Supplier compositeSupplier, Availability availability, boolean canCast, FlightType flightType, boolean canUseEarth, boolean isNocturnal, boolean canHang) implements Affine { public static final String DEFAULT_ID = "unicopia:unset"; public static final Registry REGISTRY = RegistryUtils.createDefaulted(Unicopia.id("race"), DEFAULT_ID); public static final Registry COMMAND_REGISTRY = RegistryUtils.createDefaulted(Unicopia.id("race/grantable"), DEFAULT_ID); public static final RegistryKey> REGISTRY_KEY = REGISTRY.getKey(); private static final DynamicCommandExceptionType UNKNOWN_RACE_EXCEPTION = new DynamicCommandExceptionType(id -> Text.translatable("race.unknown", id)); public static Race register(String name, Availability availability, boolean magic, FlightType flight, boolean earth, boolean nocturnal, boolean canHang) { return register(Unicopia.id(name), availability, magic, flight, earth, nocturnal, canHang); } public static Race register(Identifier id, Availability availability, boolean magic, FlightType flight, boolean earth, boolean nocturnal, boolean canHang) { Race race = Registry.register(REGISTRY, id, new Race(Suppliers.memoize(() -> new Composite(REGISTRY.get(id), null, null)), availability, magic, flight, earth, nocturnal, canHang)); if (availability.isGrantable()) { Registry.register(COMMAND_REGISTRY, id, race); } return race; } public static RegistryKeyArgumentType argument() { return RegistryKeyArgumentType.registryKey(COMMAND_REGISTRY.getKey()); } /** * The default, unset race. * This is used if there are no other races. */ public static final Race UNSET = register("unset", Availability.COMMANDS, false, FlightType.NONE, false, false, false); public static final Race HUMAN = register("human", Availability.COMMANDS, false, FlightType.NONE, false, false, false); public static final Race EARTH = register("earth", Availability.DEFAULT, false, FlightType.NONE, true, false, false); public static final Race UNICORN = register("unicorn", Availability.DEFAULT, true, FlightType.NONE, false, false, false); public static final Race PEGASUS = register("pegasus", Availability.DEFAULT, false, FlightType.AVIAN, false, false, false); public static final Race BAT = register("bat", Availability.DEFAULT, false, FlightType.AVIAN, false, true, true); public static final Race ALICORN = register("alicorn", Availability.COMMANDS, true, FlightType.AVIAN, true, false, false); public static final Race CHANGELING = register("changeling", Availability.DEFAULT, false, FlightType.INSECTOID, false, false, true); public static final Race KIRIN = register("kirin", Availability.DEFAULT, true, FlightType.NONE, false, false, false); public static final Race HIPPOGRIFF = register("hippogriff", Availability.DEFAULT, false, FlightType.AVIAN, false, false, false); public static final Race SEAPONY = register("seapony", Availability.NONE, false, FlightType.NONE, false, false, false); public static void bootstrap() {} public Composite composite() { return compositeSupplier.get(); } public Composite composite(@Nullable Race pseudo, @Nullable Race potential) { return pseudo == null && potential == null ? composite() : new Composite(this, pseudo, potential); } @Override public Affinity getAffinity() { return this == CHANGELING ? Affinity.BAD : Affinity.NEUTRAL; } public boolean hasIronGut() { return !isHuman() && this != CHANGELING; } public boolean isUnset() { return this == UNSET; } public boolean isEquine() { return !isHuman(); } public boolean isFish() { return this == SEAPONY; } public boolean isHuman() { return this == UNSET || this == HUMAN; } public boolean isDayurnal() { return !isNocturnal(); } public boolean canFly() { return !flightType().isGrounded(); } public boolean canInteractWithClouds() { return canFly() && this != CHANGELING && this != BAT && this != HIPPOGRIFF; } public Identifier getId() { return REGISTRY.getId(this); } public Text getDisplayName() { return Text.translatable(getTranslationKey()); } public Text getAltDisplayName() { return Text.translatable(getTranslationKey() + ".alt"); } public String getTranslationKey() { Identifier id = getId(); return String.format("%s.race.%s", id.getNamespace(), id.getPath().toLowerCase()); } public Identifier getIcon() { Identifier id = getId(); return new Identifier(id.getNamespace(), "textures/gui/race/" + id.getPath() + ".png"); } public boolean isPermitted(@Nullable PlayerEntity sender) { Set whitelist = Unicopia.getConfig().speciesWhiteList.get(); return this == HUMAN || isUnset() || whitelist.isEmpty() || whitelist.contains(getId().toString()); } public Race validate(PlayerEntity sender) { if (!isPermitted(sender)) { Race alternative = this == EARTH ? HUMAN : EARTH.validate(sender); if (alternative != this && sender instanceof ServerPlayerEntity spe) { spe.sendMessageToClient(Text.translatable("respawn.reason.illegal_race", getDisplayName()), false); } return alternative; } return this; } public Race or(Race other) { return isEquine() ? this : other; } @Override public int hashCode() { return getId().hashCode(); } @Override public boolean equals(Object o) { return o instanceof Race race && Objects.equal(race.getId(), getId()); } @Override public String toString() { return "Race{ " + getId().toString() + " }"; } public boolean equals(String s) { return getId().toString().equalsIgnoreCase(s) || getTranslationKey().equalsIgnoreCase(s); } public static Race fromName(String s, Race def) { if (!Strings.isNullOrEmpty(s)) { Identifier id = Identifier.tryParse(s); if (id != null) { if (id.getNamespace() == Identifier.DEFAULT_NAMESPACE) { id = new Identifier(Unicopia.DEFAULT_NAMESPACE, id.getPath()); } return REGISTRY.getOrEmpty(id).orElse(def); } } return def; } public static Race fromName(String name) { return fromName(name, EARTH); } public static Race fromArgument(CommandContext context, String name) throws CommandSyntaxException { Identifier id = context.getArgument(name, RegistryKey.class).getValue(); return REGISTRY.getOrEmpty(id).orElseThrow(() -> UNKNOWN_RACE_EXCEPTION.create(id)); } public static Set allPermitted(PlayerEntity player) { return REGISTRY.stream().filter(r -> r.isPermitted(player)).collect(Collectors.toSet()); } public record Composite (Race physical, @Nullable Race pseudo, @Nullable Race potential) { public Race collapsed() { return pseudo == null ? physical : pseudo; } public boolean includes(Race race) { return physical == race || pseudo == race; } public boolean any(Predicate test) { return test.test(physical) || (pseudo != null && test.test(pseudo)); } public boolean canUseEarth() { return any(Race::canUseEarth); } public boolean canFly() { return any(Race::canFly); } public boolean canCast() { return any(Race::canCast); } public FlightType flightType() { if (pseudo() == null) { return physical().flightType(); } return physical().flightType().or(pseudo().flightType()); } } }