1 /* 2 * Copyright (c) 2017-2019 sel-project 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in all 12 * copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 * SOFTWARE. 21 * 22 */ 23 /** 24 * Copyright: Copyright (c) 2017-2019 sel-project 25 * License: MIT 26 * Authors: Kripth 27 * Source: $(HTTP github.com/sel-project/selery/source/selery/command/util.d, selery/command/util.d) 28 */ 29 module selery.command.util; 30 31 import std.algorithm : sort; 32 import std.conv : to, ConvException; 33 import std.random : uniform; 34 import std..string : split, join, toLower, startsWith, replace; 35 import std.traits : isIntegral, isFloatingPoint; 36 import std.typecons : Tuple; 37 38 import selery.command.command : Command; 39 import selery.config : Gamemode; 40 import selery.entity.entity : Entity; 41 import selery.log : Message; 42 import selery.math.vector : EntityPosition, isVector, distance; 43 import selery.node.server : NodeServer; 44 import selery.player.player : Player; 45 import selery.world.world : World; 46 47 import transforms.snake; 48 49 /** 50 * Interface for command senders. 51 */ 52 interface CommandSender { 53 54 /** 55 * Gets the command sender's current server. 56 */ 57 public pure nothrow @property @safe @nogc shared(NodeServer) server(); 58 59 /** 60 * Gets the commands that can be called by the command sender 61 * in its current status. 62 * Aliases are included in the list and the Command object is 63 * the same as the non-aliased command. 64 * Example: 65 * --- 66 * assert(sender.availableCommands["help"] is sender.availableCommands["?"]); 67 * --- 68 */ 69 public @property Command[string] availableCommands(); 70 71 /** 72 * Sends a message to the command sender. 73 * The message can contain formatting codes and translations. 74 * Example: 75 * --- 76 * sender.sendMessage("Hello"); 77 * sender.sendMessage(Format.blue, "This is a blue message"); 78 * sender.sendMessage(Format.yellow, Translation("multiplayer.player.joined", "Steve")); 79 * --- 80 */ 81 public final void sendMessage(E...)(E args) { 82 this.sendMessageImpl(Message.convert(args)); 83 } 84 85 protected void sendMessageImpl(Message[] messages); 86 87 } 88 89 /** 90 * Interface for a command sender that is spawned in a world. 91 */ 92 interface WorldCommandSender : CommandSender { 93 94 /** 95 * Gets the command sender's world. 96 */ 97 public pure nothrow @property @safe @nogc World world(); 98 99 /** 100 * Gets the command sender's current position. 101 */ 102 public @property EntityPosition position(); 103 104 /** 105 * Gets the list of the entities visible by the 106 * command sender. 107 */ 108 public @property Entity[] visibleEntities(); 109 110 /** 111 * Gets the list of the players visible by the 112 * command sender. 113 */ 114 public @property Player[] visiblePlayers(); 115 116 } 117 118 enum PocketType { 119 120 target, 121 blockpos, 122 stringenum, 123 string, 124 rawtext, 125 integer, 126 floating, 127 boolean, 128 129 } 130 131 /** 132 * Created an enum with a single value that can be used in 133 * commands with a single argument. 134 * Example: 135 * --- 136 * // test add @a 137 * @command("test") test0(SingleEnum!"add", Target target) {} 138 * 139 * // test remove @a 140 * @command("test") test1(SingleEnum!"remove", Target target) {} 141 * --- 142 */ 143 template SingleEnum(string value) { 144 145 mixin("enum SingleEnum { " ~ value ~ " }"); 146 147 } 148 149 /** 150 * Example: 151 * --- 152 * enum Example : int { 153 * plain = 12, 154 * camelCase = 44, 155 * PascalCase = 100, 156 * ALL_UPPERCASE = 200 157 * } 158 * alias Snake = SnakeCaseEnum!Example; 159 * assert(Example.plain == Snake.plain); 160 * assert(Example.camelCase == Snake.camel_case); 161 * assert(Example.PascalCase == Snake.pascal_case); 162 * assert(Example.ALL_UPPERCASE == Snake.all_uppercase); 163 * --- 164 */ 165 template SnakeCaseEnum(T) if(is(T == enum)) { 166 167 mixin("enum SnakeCaseEnum {" ~ (){ 168 string ret; 169 foreach(immutable member ; __traits(allMembers, T)) { 170 ret ~= member.snakeCaseCT ~ "=T." ~ member ~ ","; 171 } 172 return ret; 173 }() ~ "}"); 174 175 } 176 177 struct Ranged(T, string _type, T _min, T _max) if((isIntegral!T || isFloatingPoint!T) && _min < _max && (_type == "[]" || _type == "(]" || _type == "[)" || _type == "()")) { 178 179 enum __is_range; 180 181 alias Type = T; 182 183 enum type = _type; 184 185 enum min = _min; 186 enum max = _max; 187 188 T value; 189 190 alias value this; 191 192 } 193 194 alias Ranged(T, T min, T max) = Ranged!(T, "[]", min, max); 195 196 enum isRanged(T) = __traits(hasMember, T, "__is_range"); 197 198 template minImpl(T) { 199 static if(isIntegral!T) enum minImpl = T.min; 200 else enum minImpl = T.min_normal; 201 } 202 203 /** 204 * Indicates a position with absolutes and/or relatives coordinates. 205 * Example: 206 * --- 207 * auto pos = Position(Position.Point.fromString("~"), Position.Point.fromString("1"), Position.Point.fromString("~10")); 208 * auto res = pos.from(BlockPosition(1, 10, 100)); 209 * assert(res == BlockPosition(1, 1, 110)); 210 * --- 211 */ 212 struct PositionImpl(V) if(isVector!V) { 213 214 alias T = V.Type; 215 216 static struct Point { 217 218 private bool absolute; 219 private T _value; 220 private T function(T, T) _apply; 221 222 public this(bool absolute, immutable T v) { 223 this.absolute = absolute; 224 this._value = v; 225 if(absolute) { 226 this._apply = &applyAbsolute; 227 } else { 228 this._apply = &applyRelative; 229 } 230 } 231 232 private static T applyAbsolute(T a, T b) { 233 return a; 234 } 235 236 private static T applyRelative(T a, T b) { 237 return b + a; 238 } 239 240 public T apply(T value) { 241 return this._apply(this._value, value); 242 } 243 244 public string toString() { 245 if(this.absolute) { 246 return to!string(this._value); 247 } else if(this._value == 0) { 248 return "~"; 249 } else { 250 return "~" ~ to!string(this._value); 251 } 252 } 253 254 public static Point fromString(string str) { 255 if(str.length) { 256 if(str[0] == '~') { 257 if(str.length == 1) { 258 return Point(false, 0); 259 } else { 260 return Point(false, to!T(str[1..$])); 261 } 262 } else { 263 return Point(true, to!T(str)); 264 } 265 } else { 266 return Point(true, T.init); 267 } 268 } 269 270 } 271 272 public static typeof(this) fromString(string str) { 273 auto spl = str.split(" "); 274 if(spl.length != 3) throw new ConvException("Wrong format"); 275 else return typeof(this)(Point.fromString(spl[0]), Point.fromString(spl[1]), Point.fromString(spl[2])); 276 } 277 278 mixin((){ 279 string ret; 280 foreach(c ; V.coords) { 281 ret ~= "public Point " ~ c ~ ";"; 282 } 283 return ret; 284 }()); 285 286 /** 287 * Creates a vector from an initial position (used for 288 * relative values). 289 */ 290 public @property V from(V position) { 291 T[V.coords.length] ret; 292 foreach(i, c; V.coords) { 293 mixin("ret[i] = this." ~ c ~ ".apply(position." ~ c ~ ");"); 294 } 295 return V(ret); 296 } 297 298 public string toCoordsString(string glue=", ") { 299 string[] ret; 300 foreach(c ; V.coords) { 301 ret ~= mixin("this." ~ c ~ ".toString()"); 302 } 303 return ret.join(glue); 304 } 305 306 public string toString() { 307 return "Position(" ~ this.toCoordsString() ~ ")"; 308 } 309 310 } 311 312 /// ditto 313 alias Position = PositionImpl!EntityPosition; 314 315 /** 316 * Indicates a target selected using a username or a target selector. 317 * For reference see $(LINK2 https://minecraft.gamepedia.com/Commands#Target_selector_variables, Command on Minecraft Wiki). 318 */ 319 struct Target { 320 321 /** 322 * Raw input of the selector used. 323 */ 324 public string input; 325 326 public Entity[] entities; 327 public Player[] players; 328 329 /** 330 * Indicates whether the target was a player or an entity. 331 * Example: 332 * --- 333 * "Steve" = true 334 * "@a" = true 335 * "@e" = false 336 * "@e[type=player]" = true 337 * "@r" = true 338 * "@r[type=creeper]" = false 339 * --- 340 */ 341 public bool player = true; 342 343 public this(string input) { 344 this.input = input; 345 } 346 347 public this(string input, Entity[] entities, bool player=true) { 348 this(input); 349 this.entities = entities; 350 foreach(entity ; entities) { 351 if(cast(Player)entity) this.players ~= cast(Player)entity; 352 } 353 this.player = player; 354 } 355 356 public this(string input, Player[] players, bool player=true) { 357 this(input); 358 this.entities = cast(Entity[])players; 359 this.players = players; 360 } 361 362 /** 363 * Creates a target from a username or a selector string. 364 */ 365 public static Target fromString(WorldCommandSender sender, string str) { 366 if(str.length >= 2 && str[0] == '@') { 367 string[string] selectors; 368 if(str.length >= 4 && str[2] == '[' && str[$-1] == ']') { 369 foreach(sel ; str[3..$-1].split(",")) { 370 auto spl = sel.split("="); 371 if(spl.length == 2) selectors[spl[0]] = spl[1]; 372 } 373 } 374 switch(str[1]) { 375 case 's': 376 if(cast(Entity)sender) { 377 return Target(str, [cast(Entity)sender]); 378 } else { 379 return Target(str); 380 } 381 case 'p': 382 auto players = sender.visiblePlayers; 383 if(players.length) { 384 if("c" !in selectors) { 385 selectors["c"] = "1"; 386 } 387 filter(sender, players, selectors); 388 //TODO sort per distance 389 return Target(str); 390 } else { 391 return Target(str); 392 } 393 case 'r': 394 size_t amount = 1; 395 auto c = "c" in selectors; 396 if(c) { 397 try { 398 amount = to!size_t(*c); 399 } catch(ConvException) {} 400 selectors.remove("c"); 401 } 402 Target rImpl(T:Entity)(T[] data) { 403 filter(sender, data, selectors); 404 if(amount >= data.length) { 405 return Target(str, data); 406 } else { 407 T[] selected; 408 while(--amount) { 409 size_t index = uniform(0, data.length); 410 selected ~= data[index]; 411 data = data[0..index] ~ data[index+1..$]; 412 } 413 return Target(str, selected, is(T == Player)); 414 } 415 } 416 auto type = "type" in selectors; 417 if(type && *type != "player") { 418 return rImpl(sender.visibleEntities); 419 } else { 420 return rImpl(sender.visiblePlayers); 421 } 422 case 'a': 423 auto players = sender.visiblePlayers; 424 filter(sender, players, selectors); 425 return Target(str, players, true); 426 case 'e': 427 auto entities = sender.visibleEntities; 428 filter(sender, entities, selectors); 429 return Target(str, entities, false); 430 default: 431 return Target(str); 432 } 433 } else { 434 immutable sel = str.toLower; 435 Player[] ret; 436 foreach(player ; sender.visiblePlayers) { 437 if(player.lname == sel) ret ~= player; 438 } 439 return Target(str, ret); 440 } 441 } 442 443 } 444 445 private struct Res { 446 447 bool exists; 448 bool inverted; 449 string value; 450 451 alias exists this; 452 453 } 454 455 private void filter(T:Entity)(WorldCommandSender sender, ref T[] entities, string[string] selectors) { 456 Res data(string key) { 457 auto p = key in selectors; 458 if(p) { 459 if((*p).startsWith("!")) return Res(true, true, (*p)[1..$]); 460 else return Res(true, false, *p); 461 } else { 462 return Res(false); 463 } 464 } 465 auto type = data("type"); 466 if(type) { 467 // filter type 468 if(!type.inverted) filterImpl!("entity.type == a")(entities, type.value); 469 else filterImpl!("entity.type != a")(entities, type.value); 470 } 471 auto name = data("name"); 472 if(name) { 473 // filter by nametag 474 if(!name.inverted) filterImpl!("entity.nametag == a")(entities, name.value); 475 else filterImpl!("entity.nametag != a")(entities, name.value); 476 } 477 auto rx = data("rx"); 478 if(rx) { 479 // filter by max pitch 480 try { filterImpl!("entity.pitch <= a")(entities, to!float(name.value)); } catch(ConvException) {} 481 } 482 auto rxm = data("rxm"); 483 if(rxm) { 484 // filter by min pitch 485 try { filterImpl!("entity.pitch >= a")(entities, to!float(name.value)); } catch(ConvException) {} 486 } 487 auto ry = data("ry"); 488 if(ry) { 489 // filter by max yaw 490 try { filterImpl!("entity.yaw <= a")(entities, to!float(name.value)); } catch(ConvException) {} 491 } 492 auto rym = data("rym"); 493 if(rym) { 494 // filter by min yaw 495 try { filterImpl!("entity.yaw >= a")(entities, to!float(name.value)); } catch(ConvException) {} 496 } 497 auto m = data("m"); 498 auto l = data("l"); 499 auto lm = data("lm"); 500 if(m || l || lm) { 501 static if(is(T : Player)) { 502 alias players = entities; 503 } else { 504 // filter out non-players 505 Player[] players; 506 foreach(entity ; entities) { 507 auto player = cast(Player)entity; 508 if(player !is null) players ~= player; 509 } 510 } 511 if(m) { 512 // filter gamemode 513 int gamemode = (){ 514 switch(m.value) { 515 case "0": case "s": case "survival": return 0; 516 case "1": case "c": case "creative": return 1; 517 case "2": case "a": case "adventure": return 2; 518 case "3": case "sp": case "spectator": return 3; 519 default: return -1; 520 } 521 }(); 522 if(gamemode >= 0) { 523 if(!m.inverted) filterImpl!("entity.gamemode == a")(players, gamemode); 524 else filterImpl!("entity.gamemode != a")(players, gamemode); 525 } 526 } 527 if(l) { 528 // filter xp (min) 529 try { 530 filterImpl!("entity.level <= a")(players, to!uint(l.value)); 531 } catch(ConvException) {} 532 } 533 if(lm) { 534 // filter xp (max) 535 try { 536 filterImpl!("entity.level >= a")(players, to!uint(l.value)); 537 } catch(ConvException) {} 538 } 539 static if(!is(T : Player)) { 540 entities = cast(Entity[])players; 541 } 542 } 543 auto c = data("c"); 544 if(c) { 545 try { 546 auto amount = to!ptrdiff_t(c.value); 547 if(amount > 0) { 548 entities = filterDistance!false(sender.position, entities, amount); 549 } else if(amount < 0) { 550 entities = filterDistance!true(sender.position, entities, -amount); 551 } else { 552 entities.length = 0; 553 } 554 } catch(ConvException) {} 555 } 556 } 557 558 private void filterImpl(string query, T:Entity, A)(ref T[] entities, A a) { 559 T[] ret; 560 foreach(entity ; entities) { 561 if(mixin(query)) ret ~= entity; 562 } 563 if(ret.length != entities.length) entities = ret; 564 } 565 566 private T[] filterDistance(bool inverted, T:Entity)(EntityPosition position, T[] entities, size_t count) { 567 if(count >= entities.length) return entities; 568 Tuple!(T, double)[] distances; 569 foreach(entity ; entities) { 570 distances ~= Tuple!(T, double)(entity, distance(position, entity.position)); 571 } 572 sort!((a, b) => a[1] == b[1] ? a[0].id < b[0].id : a[1] < b[1])(distances); 573 T[] ret; 574 foreach(i ; 0..count) { 575 static if(inverted) ret ~= distances[$-i-1][0]; 576 else ret ~= distances[i][0]; 577 } 578 return ret; 579 }