001/* 002 * PermissionsEx 003 * Copyright (C) zml and PermissionsEx contributors 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package ca.stellardrift.permissionsex.minecraft.command.definition; 018 019import ca.stellardrift.permissionsex.context.ContextValue; 020import ca.stellardrift.permissionsex.minecraft.command.ButtonType; 021import ca.stellardrift.permissionsex.minecraft.command.CommandException; 022import ca.stellardrift.permissionsex.minecraft.command.CommandRegistrationContext; 023import ca.stellardrift.permissionsex.minecraft.command.Commander; 024import ca.stellardrift.permissionsex.minecraft.command.Elements; 025import ca.stellardrift.permissionsex.minecraft.command.Permission; 026import ca.stellardrift.permissionsex.minecraft.command.argument.Parsers; 027import ca.stellardrift.permissionsex.rank.RankLadder; 028import ca.stellardrift.permissionsex.subject.SubjectRef; 029import ca.stellardrift.permissionsex.subject.SubjectType; 030import cloud.commandframework.Command; 031import cloud.commandframework.arguments.CommandArgument; 032import cloud.commandframework.arguments.flags.CommandFlag; 033import cloud.commandframework.arguments.standard.IntegerArgument; 034import cloud.commandframework.minecraft.extras.MinecraftExtrasMetaKeys; 035import cloud.commandframework.minecraft.extras.RichDescription; 036import io.leangen.geantyref.TypeToken; 037import net.kyori.adventure.text.Component; 038import org.checkerframework.checker.nullness.qual.Nullable; 039 040import java.util.ArrayList; 041import java.util.List; 042import java.util.ListIterator; 043import java.util.Set; 044 045import static ca.stellardrift.permissionsex.minecraft.command.Elements.*; 046import static net.kyori.adventure.text.Component.text; 047import static net.kyori.adventure.text.TextComponent.ofChildren; 048 049public final class RankingCommands { 050 051 // All commands 052 053 private static CommandArgument<Commander, RankLadder> ladderArgument(final boolean required) { 054 final CommandArgument.Builder<Commander, RankLadder> ladder = CommandArgument.<Commander, RankLadder>ofType(RankLadder.class, "ladder") 055 .withParser(Parsers.rankLadder()); 056 if (!required) { 057 ladder.asOptionalWithDefault("default"); 058 } 059 return ladder.build(); 060 } 061 062 private static final CommandArgument<Commander, RankLadder> LADDER_ARG = ladderArgument(true); 063 064 // Only for subcommands that modify 065 private static final CommandFlag<Void> FLAG_RELATIVE = CommandFlag.newBuilder("relative") 066 .withDescription(RichDescription.of(Messages.RANK_ARG_RELATIVE_DESCRIPTION)) 067 .withAliases("r") 068 .build(); 069 070 private RankingCommands() { 071 } 072 073 static void register(final CommandRegistrationContext ctx) { 074 // ctx.register(RankingCommands::list, "") 075 ctx.push(ctx.head().argument(LADDER_ARG, RichDescription.of(Messages.RANK_ARG_LADDER_DESCRIPTION)), child -> { 076 ctx.register(list(ctx.head())); 077 ctx.register(RankingCommands::add, "add", "+"); 078 ctx.register(RankingCommands::remove, "remove", "rem", "delete", "del", "-"); 079 }); 080 } 081 082 static Command.Builder<Commander> list(final Command.Builder<Commander> builder) { 083 final Permission listPerm = Permission.pex("ranking.list"); 084 return builder 085 .meta(MinecraftExtrasMetaKeys.DESCRIPTION, Messages.RANK_LIST_DESCRIPTION.tr()) 086 .permission(listPerm) 087 .handler(ctx -> { 088 final Commander source = ctx.getSender(); 089 final RankLadder ladder = ctx.get(LADDER_ARG); 090 final List<Component> ranksList = new ArrayList<>(); 091 final List<? extends SubjectRef<?>> rawRanks = ladder.ranks(); 092 093 // Build list 094 if (rawRanks.size() == 0) { 095 throw new CommandException(Messages.RANKING_ERROR_EMPTY_LADDER.tr(ladder)); 096 } else if (rawRanks.size() == 1) { 097 ranksList.add(source.formatter().subject(rawRanks.get(0))); 098 } else { 099 for (final ListIterator<? extends SubjectRef<?>> it = rawRanks.listIterator(rawRanks.size()); it.hasPrevious(); ) { 100 final int idx = it.previousIndex(); 101 final SubjectRef<?> rank = it.previous(); 102 final Component rendered; 103 if (idx == rawRanks.size()) { // first 104 rendered = ofChildren( 105 source.formatter().subject(rank), 106 moveDownButton(source, ladder, rank), 107 deleteButton(source, ladder, rank) 108 ); 109 } else if (idx == 0) { // last 110 rendered = ofChildren( 111 source.formatter().subject(rank), 112 moveUpButton(source, ladder, rank), 113 deleteButton(source, ladder, rank) 114 ); 115 } else { // in-between 116 rendered = ofChildren( 117 source.formatter().subject(rank), 118 moveDownButton(source, ladder, rank), 119 moveUpButton(source, ladder, rank), 120 deleteButton(source, ladder, rank) 121 ); 122 } 123 ranksList.add(rendered); 124 } 125 } 126 127 // Send as a pagination 128 source.sendPaginated( 129 Messages.RANKING_PAGINATION_HEADER.tr( 130 ladder.name(), 131 source.formatter().button( 132 text().content("+"), 133 ButtonType.POSITIVE, 134 Messages.RANKING_BUTTON_ADD_DESCRIPTION, 135 "/pex rank " + ladder.name() + " add ", 136 false 137 ) 138 ), 139 Messages.RANKING_PAGINATION_SUBTITLE, 140 ranksList 141 ); 142 }); 143 } 144 145 private static Component deleteButton( 146 final Commander cmd, 147 final RankLadder rank, 148 final SubjectRef<?> subject 149 ) { 150 return cmd.formatter().button( 151 text().content("-"), 152 ButtonType.NEGATIVE, 153 Messages.RANKING_BUTTON_DELETE_DESCRIPTION.tr(), 154 "/pex rank " + rank.name() + " remove " + subject.type().name() + " " + subject.serializedIdentifier(), 155 true 156 ); 157 } 158 159 private static Component moveDownButton( 160 final Commander cmd, 161 final RankLadder rank, 162 final SubjectRef<?> subject 163 ) { 164 return cmd.formatter().button( 165 text().content("▼"), 166 ButtonType.NEUTRAL, 167 Messages.RANKING_BUTTON_MOVE_DOWN_DESCRIPTION.tr(), 168 "/pex rank " + rank.name() + " add " + subject.type().name() + " " + subject.serializedIdentifier() + " -r -1", 169 true 170 ); 171 } 172 173 private static Component moveUpButton( 174 final Commander cmd, 175 final RankLadder ladder, 176 final SubjectRef<?> subject 177 ) { 178 return cmd.formatter().button( 179 text().content("▲"), 180 ButtonType.NEUTRAL, 181 Messages.RANKING_BUTTON_MOVE_UP_DESCRIPTION.tr(), 182 "/pex rank " + ladder.name() + " add " + subject.type().name() + " " + subject.serializedIdentifier() + " -r 1", 183 true 184 ); 185 } 186 187 // Other subcommands 188 189 private static CommandArgument<Commander, SubjectType<?>> rankTypeArgument() { 190 return CommandArgument.<Commander, SubjectType<?>>ofType(new TypeToken<SubjectType<?>>() {}, "type") 191 .withParser(Parsers.subjectType()) 192 .build(); 193 } 194 195 private static CommandArgument<Commander, ?> rankIdentifierArgument(final CommandArgument<Commander, SubjectType<?>> parentType) { 196 return CommandArgument.<Commander, Object>ofType(Object.class, "identifier") 197 .withParser(Parsers.subjectIdentifier(data -> data.get(parentType))) 198 .build(); 199 } 200 201 202 static Command.Builder<Commander> add(final Command.Builder<Commander> builder) { 203 final Permission perm = Permission.pex("rank.add"); 204 final CommandArgument<Commander, SubjectType<?>> rankTypeArg = rankTypeArgument(); 205 final CommandArgument<Commander, ?> rankIdentifierArg = rankIdentifierArgument(rankTypeArg); 206 final CommandArgument<Commander, Integer> positionArg = IntegerArgument.optional("position"); 207 final SubjectRefProvider rankProvider = SubjectRefProvider.of(rankTypeArg, rankIdentifierArg); 208 209 return builder 210 .flag(FLAG_RELATIVE) 211 .argument(rankTypeArg) 212 .argument(rankIdentifierArg) 213 .argument(positionArg) 214 .permission(perm) 215 .meta(MinecraftExtrasMetaKeys.DESCRIPTION, Messages.RANK_ADD_DESCRIPTION.tr()) 216 .handler(handler((source, engine, ctx) -> { 217 final RankLadder ladder = ctx.get(LADDER_ARG); 218 source.checkPermission(perm.then(ladder.name())); 219 final SubjectRef<?> toAdd = rankProvider.provide(ctx); 220 final @Nullable Integer position = ctx.getOrDefault(positionArg, null); 221 222 if (position != null) { 223 int addPosition = position; 224 if (ctx.flags().isPresent(FLAG_RELATIVE.getName())) { 225 final int currentIndex = ladder.indexOf(toAdd); 226 if (currentIndex == -1) { 227 throw new CommandException(Messages.RANKING_ADD_ERROR_RELATIVE_ON_OUTSIDE_SUBJECT.tr()); 228 } 229 addPosition = (currentIndex + addPosition > 1) ? addPosition + 1 : addPosition; // If we are adding to later, we need to add after the next rank (otherwise we end up staying in the same place) 230 } 231 engine.ladders().set(ladder.name(), ladder.with(toAdd, addPosition)) 232 .whenComplete(messageSender(source, Messages.RANKING_ADD_SUCCESS_POSITION.tr( 233 source.formatter().subject(toAdd), 234 ladder.asComponent(), 235 text(addPosition) 236 ))); 237 } else { 238 // Append 239 engine.ladders().set(ladder.name(), ladder.with(toAdd)) 240 .whenComplete(messageSender(source, Messages.RANKING_ADD_SUCCESS.tr( 241 source.formatter().subject(toAdd), 242 ladder.asComponent() 243 ))); 244 } 245 })); 246 } 247 248 static Command.Builder<Commander> remove(final Command.Builder<Commander> builder) { 249 final Permission perm = Permission.pex("rank.remove"); 250 final CommandArgument<Commander, SubjectType<?>> rankTypeArg = rankTypeArgument(); 251 final CommandArgument<Commander, ?> rankIdentifierArg = rankIdentifierArgument(rankTypeArg); 252 final SubjectRefProvider rankProvider = SubjectRefProvider.of(rankTypeArg, rankIdentifierArg); 253 254 return builder 255 .argument(rankTypeArg) 256 .argument(rankIdentifierArg) 257 .permission(perm) 258 .meta(MinecraftExtrasMetaKeys.DESCRIPTION, Messages.RANK_REMOVE_DESCRIPTION.tr()) 259 .handler(handler((source, engine, ctx) -> { 260 final RankLadder ladder = ctx.get(LADDER_ARG); 261 source.checkPermission(perm.then(ladder.name())); 262 final SubjectRef<?> toRemove = rankProvider.provide(ctx); 263 final RankLadder newLadder = ladder.without(toRemove); 264 265 if (newLadder == ladder) { 266 throw new CommandException(Messages.RANKING_REMOVE_ERROR_NOT_IN_LADDER.tr( 267 source.formatter().subject(toRemove), 268 ladder.asComponent() 269 )); 270 } else { 271 engine.ladders().set(ladder.name(), newLadder) 272 .whenComplete(messageSender(source, Messages.RANKING_REMOVE_SUCCESS.tr( 273 source.formatter().subject(toRemove), 274 ladder.asComponent() 275 ))); 276 } 277 })); 278 } 279 280 public static Command.Builder<Commander> promote(final Command.Builder<Commander> builder) { 281 final Permission promote = Permission.pex("promote"); 282 final CommandArgument<Commander, SubjectType<?>> rankTypeArg = rankTypeArgument(); 283 final CommandArgument<Commander, ?> rankIdentifierArg = rankIdentifierArgument(rankTypeArg); 284 final CommandArgument<Commander, RankLadder> ladderArg = ladderArgument(false); 285 final SubjectRefProvider rankProvider = SubjectRefProvider.of(rankTypeArg, rankIdentifierArg); 286 287 return builder 288 .flag(FLAG_TRANSIENT) 289 .flag(FLAG_CONTEXT) 290 .argument(rankTypeArg) 291 .argument(rankIdentifierArg) 292 .argument(ladderArg) 293 .meta(MinecraftExtrasMetaKeys.DESCRIPTION, Messages.RANK_PROMOTE_DESCRIPTION.tr()) 294 .permission(promote) 295 .handler(handler((source, engine, ctx) -> { 296 final RankLadder ladder = ctx.get(ladderArg); 297 final Set<ContextValue<?>> contexts = Elements.contexts(ctx); 298 final SubjectRef.ToData<?> subject = rankProvider.provideData(ctx, promote); 299 subject.update(data -> ladder.promote(contexts, data)) 300 .thenAccept(change -> { 301 if (!change.changed()) { 302 throw new CommandException(Messages.PROMOTE_ERROR_ALREADY_AT_TOP.tr( 303 source.formatter().subject(subject), 304 ladder.asComponent() 305 )); 306 } 307 }).whenComplete(messageSender(source, Messages.PROMOTE_SUCCESS.tr( 308 source.formatter().subject(subject), 309 ladder.asComponent().style(source.formatter()::hl) 310 ))); 311 })); 312 } 313 314 public static Command.Builder<Commander> demote(final Command.Builder<Commander> builder) { 315 final Permission promote = Permission.pex("demote"); 316 final CommandArgument<Commander, SubjectType<?>> rankTypeArg = rankTypeArgument(); 317 final CommandArgument<Commander, ?> rankIdentifierArg = rankIdentifierArgument(rankTypeArg); 318 final CommandArgument<Commander, RankLadder> ladderArg = ladderArgument(false); 319 final SubjectRefProvider rankProvider = SubjectRefProvider.of(rankTypeArg, rankIdentifierArg); 320 321 return builder 322 .flag(FLAG_TRANSIENT) 323 .flag(FLAG_CONTEXT) 324 .argument(rankTypeArg) 325 .argument(rankIdentifierArg) 326 .argument(ladderArg) 327 .meta(MinecraftExtrasMetaKeys.DESCRIPTION, Messages.RANK_DEMOTE_DESCRIPTION.tr()) 328 .permission(promote) 329 .handler(handler((source, engine, ctx) -> { 330 final RankLadder ladder = ctx.get(ladderArg); 331 final Set<ContextValue<?>> contexts = Elements.contexts(ctx); 332 final SubjectRef.ToData<?> subject = rankProvider.provideData(ctx, promote); 333 subject.update(data -> ladder.promote(contexts, data)) 334 .thenAccept(change -> { 335 if (!change.changed()) { 336 throw new CommandException(Messages.DEMOTE_ERROR_NOT_ON_LADDER.tr( 337 source.formatter().subject(subject), 338 ladder.asComponent() 339 )); 340 } 341 }).whenComplete(messageSender(source, Messages.DEMOTE_SUCCESS.tr( 342 source.formatter().subject(subject), 343 ladder.asComponent().style(source.formatter()::hl) 344 ))); 345 })); 346 } 347 348}