package tc.oc.pgm.portals; import java.util.Optional; import java.util.logging.Logger; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.TranslatableComponent; import org.bukkit.util.Vector; import org.jdom2.Attribute; import org.jdom2.Document; import org.jdom2.Element; import tc.oc.commons.core.util.Optionals; import tc.oc.pgm.eventrules.EventRule; import tc.oc.pgm.eventrules.EventRuleModule; import tc.oc.pgm.eventrules.EventRuleScope; import tc.oc.pgm.filters.Filter; import tc.oc.pgm.filters.matcher.StaticFilter; import tc.oc.pgm.filters.operator.InverseFilter; import tc.oc.pgm.filters.parser.FilterParser; import tc.oc.pgm.filters.query.IPlayerQuery; import tc.oc.pgm.map.MapModule; import tc.oc.pgm.map.MapModuleContext; import tc.oc.pgm.module.ModuleDescription; import tc.oc.pgm.regions.RandomPointsValidation; import tc.oc.pgm.regions.Region; import tc.oc.pgm.regions.RegionParser; import tc.oc.pgm.regions.TranslatedRegion; import tc.oc.pgm.regions.Union; import tc.oc.pgm.utils.XMLUtils; import tc.oc.pgm.xml.InvalidXMLException; @ModuleDescription(name="Portal", depends={ EventRuleModule.class }) public class PortalModule implements MapModule { private static final BaseComponent PROTECT_MESSAGE = new TranslatableComponent("match.portal.protectMessage"); // --------------------- // ---- XML Parsing ---- // --------------------- public static PortalModule parse(MapModuleContext context, Logger logger, Document doc) throws InvalidXMLException { final FilterParser filterParser = context.needModule(FilterParser.class); final RegionParser regionParser = context.needModule(RegionParser.class); for(Element portalEl : XMLUtils.flattenElements(doc.getRootElement(), "portals", "portal")) { // Piecewise transform PortalTransform transform = PortalTransform.piecewise(parseDoubleTransform(portalEl, "x", DoubleTransform.IDENTITY), parseDoubleTransform(portalEl, "y", DoubleTransform.IDENTITY), parseDoubleTransform(portalEl, "z", DoubleTransform.IDENTITY), parseDoubleTransform(portalEl, "yaw", DoubleTransform.IDENTITY), parseDoubleTransform(portalEl, "pitch", DoubleTransform.IDENTITY)); // Optional entrance region (required for old proto) final Optional<Region> entrance = regionParser.property(portalEl) .legacy() .optionalUnion(); // Optional exit region Optional<Region> exit = regionParser.property(portalEl, "destination") .validate(RandomPointsValidation.INSTANCE) .optional(); if(exit.isPresent()) { // If there is an explicit exit region, create a transform for it and combine // it with the piecewise transform (so angle transforms are still applied). transform = PortalTransform.concatenate(transform, PortalTransform.regional(entrance, exit.get())); } else if(entrance.isPresent() && transform.invertible()) { // If no exit region is specified, but there is an entrance region, and the // piecewise transform is invertible, infer the exit region from the entrance region. exit = Optional.of(new PortalExitRegion(entrance.get(), transform)); } // Dynamic filters final Optional<Filter> forward = filterParser.property(portalEl, "forward").respondsTo(IPlayerQuery.class).dynamic().optional(); final Optional<Filter> reverse = filterParser.property(portalEl, "reverse").respondsTo(IPlayerQuery.class).dynamic().optional(); final Optional<Filter> transit = filterParser.property(portalEl, "transit").respondsTo(IPlayerQuery.class).dynamic().optional(); // Check for conflicting dynamic filters if(transit.isPresent() && (forward.isPresent() || reverse.isPresent())) { throw new InvalidXMLException("Cannot combine 'transit' property with 'forward' or 'transit' properties", portalEl); } // Check for conflicting region and dynamic filter at each end of the portal if(entrance.isPresent() && (forward.isPresent() || transit.isPresent())) { throw new InvalidXMLException("Cannot combine an entrance region with 'forward' or 'transit' properties", portalEl); } if(exit.isPresent() && (reverse.isPresent() || transit.isPresent())) { throw new InvalidXMLException("Cannot combine an exit region with 'reverse' or 'transit' properties", portalEl); } // Figure out the forward trigger, from the dynamic filters or entrance region final Filter forwardFinal = Optionals.first(forward, transit, entrance) .orElseThrow(() -> new InvalidXMLException("Portal must have an entrance region, or one of 'forward' or 'transit' properties", portalEl)); // Figure out the (optional) reverse trigger, from dynamic filters or exit region final Optional<Filter> reverseFinal = Optionals.first(reverse, transit.map(InverseFilter::new), exit); // Portal is always bidirectional if a reverse dynamic filter is specified, // otherwise it must be enabled explicitly. final boolean bidirectional = reverse.isPresent() || transit.isPresent() || XMLUtils.parseBoolean(portalEl, "bidirectional").optional(false); if(bidirectional && !transform.invertible()) { throw new InvalidXMLException("Bidirectional portal must have an invertible transform", portalEl); } // Passive filters final Filter participantFilter = filterParser.property(portalEl, "filter") .optional(StaticFilter.ALLOW); final Filter observerFilter = filterParser.property(portalEl, "observers") .optional(StaticFilter.ALLOW); boolean sound = XMLUtils.parseBoolean(portalEl.getAttribute("sound"), true); boolean smooth = XMLUtils.parseBoolean(portalEl.getAttribute("smooth"), false); // Protect the entrance/exit final Attribute attrProtect = portalEl.getAttribute("protect"); if(XMLUtils.parseBoolean(attrProtect, false)) { entrance.orElseThrow(() -> new InvalidXMLException("Cannot protect a portal without an entrance region", attrProtect)); protectRegion(context, entrance.get()); exit.ifPresent(r -> protectRegion(context, r)); } context.features().define(portalEl, new PortalImpl(forwardFinal, transform, participantFilter, observerFilter, sound, smooth)); if(bidirectional) { context.features().define(portalEl, new PortalImpl(reverseFinal.get(), transform.inverse(), participantFilter, observerFilter, sound, smooth)); } } return context.features().containsAny(Portal.class) ? new PortalModule() : null; } /** * Use an {@link EventRule} to protect the given entrance/exit {@link Region}. * * The region is extended up by 2m to allow for the height of the player. */ private static void protectRegion(MapModuleContext context, Region region) { region = Union.of(region, TranslatedRegion.translate(region, new Vector(0, 1, 0)), TranslatedRegion.translate(region, new Vector(0, 2, 0))); context.needModule(EventRuleModule.class) .eventRuleContext() .prepend(EventRule.newEventFilter(EventRuleScope.BLOCK_PLACE, region, StaticFilter.DENY, PROTECT_MESSAGE, false)); } private static DoubleTransform parseDoubleTransform(Element el, String attributeName, DoubleTransform def) throws InvalidXMLException { Attribute attr = el.getAttribute(attributeName); if(attr == null) { return def; } String text = attr.getValue(); try { if(text.startsWith("@")) { double value = Double.parseDouble(text.substring(1)); return new DoubleTransform.Constant(value); } else { double value = Double.parseDouble(text); return new DoubleTransform.Translate(value); } } catch(NumberFormatException e) { throw new InvalidXMLException("Invalid portal coordinate", attr, e); } } }