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