001//
002// MIT License
003//
004// Copyright (c) 2021 Alexander Söderberg & Contributors
005//
006// Permission is hereby granted, free of charge, to any person obtaining a copy
007// of this software and associated documentation files (the "Software"), to deal
008// in the Software without restriction, including without limitation the rights
009// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010// copies of the Software, and to permit persons to whom the Software is
011// furnished to do so, subject to the following conditions:
012//
013// The above copyright notice and this permission notice shall be included in all
014// copies or substantial portions of the Software.
015//
016// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
022// SOFTWARE.
023//
024package cloud.commandframework.bukkit.parsers.location;
025
026import cloud.commandframework.ArgumentDescription;
027import cloud.commandframework.arguments.CommandArgument;
028import cloud.commandframework.arguments.parser.ArgumentParseResult;
029import cloud.commandframework.arguments.parser.ArgumentParser;
030import cloud.commandframework.arguments.standard.IntegerArgument;
031import cloud.commandframework.bukkit.BukkitCaptionKeys;
032import cloud.commandframework.captions.Caption;
033import cloud.commandframework.captions.CaptionVariable;
034import cloud.commandframework.context.CommandContext;
035import cloud.commandframework.exceptions.parsing.ParserException;
036import io.leangen.geantyref.TypeToken;
037import org.bukkit.Bukkit;
038import org.bukkit.Location;
039import org.bukkit.command.BlockCommandSender;
040import org.bukkit.command.CommandSender;
041import org.bukkit.entity.Entity;
042import org.bukkit.util.Vector;
043import org.checkerframework.checker.nullness.qual.NonNull;
044import org.checkerframework.checker.nullness.qual.Nullable;
045
046import java.util.Collection;
047import java.util.LinkedList;
048import java.util.List;
049import java.util.Queue;
050import java.util.function.BiFunction;
051import java.util.stream.Collectors;
052
053/**
054 * Argument parser that parses {@link Location} from three doubles. This will use the command
055 * senders world when it exists, or else it'll use the first loaded Bukkit world
056 *
057 * @param <C> Command sender type
058 * @since 1.1.0
059 */
060public final class LocationArgument<C> extends CommandArgument<C, Location> {
061
062    private LocationArgument(
063            final boolean required,
064            final @NonNull String name,
065            final @NonNull String defaultValue,
066            final @Nullable BiFunction<CommandContext<C>, String, List<String>> suggestionsProvider,
067            final @NonNull ArgumentDescription defaultDescription,
068            final @NonNull Collection<@NonNull BiFunction<@NonNull CommandContext<C>,
069                    @NonNull Queue<@NonNull String>, @NonNull ArgumentParseResult<Boolean>>> argumentPreprocessors
070    ) {
071        super(
072                required,
073                name,
074                new LocationParser<>(),
075                defaultValue,
076                TypeToken.get(Location.class),
077                suggestionsProvider,
078                defaultDescription,
079                argumentPreprocessors
080        );
081    }
082
083    /**
084     * Create a new argument builder
085     *
086     * @param name Argument name
087     * @param <C>  Command sender type
088     * @return Builder instance
089     */
090    public static <C> @NonNull Builder<C> newBuilder(
091            final @NonNull String name
092    ) {
093        return new Builder<>(name);
094    }
095
096    /**
097     * Create a new required argument
098     *
099     * @param name Argument name
100     * @param <C>  Command sender type
101     * @return Constructed argument
102     */
103    public static <C> @NonNull CommandArgument<C, Location> of(
104            final @NonNull String name
105    ) {
106        return LocationArgument.<C>newBuilder(
107                name
108        ).asRequired().build();
109    }
110
111    /**
112     * Create a new optional argument
113     *
114     * @param name Argument name
115     * @param <C>  Command sender type
116     * @return Constructed argument
117     */
118    public static <C> @NonNull CommandArgument<C, Location> optional(
119            final @NonNull String name
120    ) {
121        return LocationArgument.<C>newBuilder(
122                name
123        ).asOptional().build();
124    }
125
126
127    public static final class Builder<C> extends CommandArgument.Builder<C, Location> {
128
129        private Builder(
130                final @NonNull String name
131        ) {
132            super(
133                    TypeToken.get(Location.class),
134                    name
135            );
136        }
137
138        @Override
139        public @NonNull CommandArgument<@NonNull C, @NonNull Location> build() {
140            return new LocationArgument<>(
141                    this.isRequired(),
142                    this.getName(),
143                    this.getDefaultValue(),
144                    this.getSuggestionsProvider(),
145                    this.getDefaultDescription(),
146                    new LinkedList<>()
147            );
148        }
149
150    }
151
152
153    public static final class LocationParser<C> implements ArgumentParser<C, Location> {
154
155        private static final int EXPECTED_PARAMETER_COUNT = 3;
156
157        private final LocationCoordinateParser<C> locationCoordinateParser = new LocationCoordinateParser<>();
158
159        @Override
160        public @NonNull ArgumentParseResult<@NonNull Location> parse(
161                final @NonNull CommandContext<@NonNull C> commandContext,
162                final @NonNull Queue<@NonNull String> inputQueue
163        ) {
164            if (inputQueue.size() < 3) {
165                final StringBuilder input = new StringBuilder();
166                for (int i = 0; i < inputQueue.size(); i++) {
167                    input.append(((LinkedList<String>) inputQueue).get(i));
168                    if ((i + 1) < inputQueue.size()) {
169                        input.append(" ");
170                    }
171                }
172                return ArgumentParseResult.failure(
173                        new LocationParseException(
174                                commandContext,
175                                LocationParseException.FailureReason.WRONG_FORMAT,
176                                input.toString()
177                        )
178                );
179            }
180            final LocationCoordinate[] coordinates = new LocationCoordinate[3];
181            for (int i = 0; i < 3; i++) {
182                final ArgumentParseResult<@NonNull LocationCoordinate> coordinate = this.locationCoordinateParser.parse(
183                        commandContext,
184                        inputQueue
185                );
186                if (coordinate.getFailure().isPresent()) {
187                    return ArgumentParseResult.failure(
188                            coordinate.getFailure().get()
189                    );
190                }
191                coordinates[i] = coordinate.getParsedValue().orElseThrow(NullPointerException::new);
192            }
193            final Location originalLocation;
194            final CommandSender bukkitSender = commandContext.get("BukkitCommandSender");
195
196            if (bukkitSender instanceof BlockCommandSender) {
197                originalLocation = ((BlockCommandSender) bukkitSender).getBlock().getLocation();
198            } else if (bukkitSender instanceof Entity) {
199                originalLocation = ((Entity) bukkitSender).getLocation();
200            } else {
201                originalLocation = new Location(Bukkit.getWorlds().get(0), 0, 0, 0);
202            }
203
204            if (((coordinates[0].getType() == LocationCoordinateType.LOCAL)
205                    != (coordinates[1].getType() == LocationCoordinateType.LOCAL))
206                    || ((coordinates[0].getType() == LocationCoordinateType.LOCAL)
207                    != (coordinates[2].getType() == LocationCoordinateType.LOCAL))
208            ) {
209                return ArgumentParseResult.failure(
210                        new LocationParseException(
211                                commandContext,
212                                LocationParseException.FailureReason.MIXED_LOCAL_ABSOLUTE,
213                                ""
214                        )
215                );
216            }
217
218            if (coordinates[0].getType() == LocationCoordinateType.ABSOLUTE) {
219                originalLocation.setX(coordinates[0].getCoordinate());
220            } else if (coordinates[0].getType() == LocationCoordinateType.RELATIVE) {
221                originalLocation.add(coordinates[0].getCoordinate(), 0, 0);
222            }
223
224            if (coordinates[1].getType() == LocationCoordinateType.ABSOLUTE) {
225                originalLocation.setY(coordinates[1].getCoordinate());
226            } else if (coordinates[1].getType() == LocationCoordinateType.RELATIVE) {
227                originalLocation.add(0, coordinates[1].getCoordinate(), 0);
228            }
229
230            if (coordinates[2].getType() == LocationCoordinateType.ABSOLUTE) {
231                originalLocation.setZ(coordinates[2].getCoordinate());
232            } else if (coordinates[2].getType() == LocationCoordinateType.RELATIVE) {
233                originalLocation.add(0, 0, coordinates[2].getCoordinate());
234            } else {
235                final Vector declaredPos = new Vector(
236                        coordinates[0].getCoordinate(),
237                        coordinates[1].getCoordinate(),
238                        coordinates[2].getCoordinate()
239                );
240                return ArgumentParseResult.success(
241                        toLocalSpace(originalLocation, declaredPos)
242                );
243            }
244
245            return ArgumentParseResult.success(
246                    originalLocation
247            );
248        }
249
250        static @NonNull Location toLocalSpace(final @NonNull Location originalLocation, final @NonNull Vector declaredPos) {
251            final double cosYaw = Math.cos(toRadians(originalLocation.getYaw() + 90.0F));
252            final double sinYaw = Math.sin(toRadians(originalLocation.getYaw() + 90.0F));
253            final double cosPitch = Math.cos(toRadians(-originalLocation.getPitch()));
254            final double sinPitch = Math.sin(toRadians(-originalLocation.getPitch()));
255            final double cosNegYaw = Math.cos(toRadians(-originalLocation.getPitch() + 90.0F));
256            final double sinNegYaw = Math.sin(toRadians(-originalLocation.getPitch() + 90.0F));
257            final Vector zModifier = new Vector(cosYaw * cosPitch, sinPitch, sinYaw * cosPitch);
258            final Vector yModifier = new Vector(cosYaw * cosNegYaw, sinNegYaw, sinYaw * cosNegYaw);
259            final Vector xModifier = zModifier.crossProduct(yModifier).multiply(-1);
260            final double xOffset = dotProduct(declaredPos, xModifier.getX(), yModifier.getX(), zModifier.getX());
261            final double yOffset = dotProduct(declaredPos, xModifier.getY(), yModifier.getY(), zModifier.getY());
262            final double zOffset = dotProduct(declaredPos, xModifier.getZ(), yModifier.getZ(), zModifier.getZ());
263            return originalLocation.add(xOffset, yOffset, zOffset);
264        }
265
266        private static double dotProduct(final Vector location, final double x, final double y, final double z) {
267            return location.getX() * x + location.getY() * y + location.getZ() * z;
268        }
269
270        private static float toRadians(final float degrees) {
271            return degrees * (float) Math.PI / 180f;
272        }
273
274        @Override
275        public @NonNull List<@NonNull String> suggestions(
276                final @NonNull CommandContext<C> commandContext,
277                final @NonNull String input
278        ) {
279            return LocationArgument.LocationParser.getSuggestions(commandContext, input);
280        }
281
282        static <C> @NonNull List<@NonNull String> getSuggestions(
283                final @NonNull CommandContext<C> commandContext,
284                final @NonNull String input
285        ) {
286            final String workingInput;
287            final String prefix;
288            if (input.startsWith("~") || input.startsWith("^")) {
289                prefix = Character.toString(input.charAt(0));
290                workingInput = input.substring(1);
291            } else {
292                prefix = "";
293                workingInput = input;
294            }
295            return IntegerArgument.IntegerParser.getSuggestions(
296                    Integer.MIN_VALUE,
297                    Integer.MAX_VALUE,
298                    workingInput
299            ).stream().map(string -> prefix + string).collect(Collectors.toList());
300        }
301
302        @Override
303        public int getRequestedArgumentCount() {
304            return EXPECTED_PARAMETER_COUNT;
305        }
306
307    }
308
309
310    static class LocationParseException extends ParserException {
311
312        private static final long serialVersionUID = -3261835227265878218L;
313
314        protected LocationParseException(
315                final @NonNull CommandContext<?> context,
316                final @NonNull FailureReason reason,
317                final @NonNull String input
318        ) {
319            super(
320                    LocationParser.class,
321                    context,
322                    reason.getCaption(),
323                    CaptionVariable.of("input", input)
324            );
325        }
326
327
328        /**
329         * Reasons for which location parsing may fail
330         */
331        public enum FailureReason {
332
333            WRONG_FORMAT(BukkitCaptionKeys.ARGUMENT_PARSE_FAILURE_LOCATION_INVALID_FORMAT),
334            MIXED_LOCAL_ABSOLUTE(BukkitCaptionKeys.ARGUMENT_PARSE_FAILURE_LOCATION_MIXED_LOCAL_ABSOLUTE);
335
336
337            private final Caption caption;
338
339            FailureReason(final @NonNull Caption caption) {
340                this.caption = caption;
341            }
342
343            /**
344             * Get the caption used for this failure reason
345             *
346             * @return The caption
347             */
348            public @NonNull Caption getCaption() {
349                return this.caption;
350            }
351
352        }
353
354    }
355
356}