ForAll command! (#4982)
* ForAll command! Implements "Bad Query Language", which has a few specifiers you can use for badminning, namely: named <regex> prototyped <prototype name> with <component name> tagged <tag name> parented_to <parent entity uid> For example: forall prototyped MobHuman parented_to 855 do explode $WX $WY 1 1 1 1; addcomp $ID Item * oops * fix a silent parsing bug, make parser louder. * cleanup * rename shit
This commit is contained in:
166
Content.Server/Administration/Commands/BQL/BqlParser.cs
Normal file
166
Content.Server/Administration/Commands/BQL/BqlParser.cs
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Content.Shared.Tag;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
|
||||||
|
// this is all really shit but it works and only runs once a command.
|
||||||
|
namespace Content.Server.Administration.Commands.BQL
|
||||||
|
{
|
||||||
|
public static class BqlParser
|
||||||
|
{
|
||||||
|
private enum TokenKind
|
||||||
|
{
|
||||||
|
With,
|
||||||
|
Named,
|
||||||
|
ParentedTo,
|
||||||
|
Prototyped,
|
||||||
|
Tagged,
|
||||||
|
Do,
|
||||||
|
String,
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly struct Token
|
||||||
|
{
|
||||||
|
public readonly TokenKind Kind;
|
||||||
|
public readonly string Text;
|
||||||
|
|
||||||
|
private Token(TokenKind kind, string text)
|
||||||
|
{
|
||||||
|
Kind = kind;
|
||||||
|
Text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
//I didn't want to write a proper parser. --moony
|
||||||
|
public static Tuple<string, Token> ExtractOneToken(string inp)
|
||||||
|
{
|
||||||
|
inp = inp.TrimStart();
|
||||||
|
return inp switch
|
||||||
|
{
|
||||||
|
_ when inp.StartsWith("with ") => new Tuple<string, Token>(inp[4..], new Token(TokenKind.With, "with")),
|
||||||
|
_ when inp.StartsWith("named ") => new Tuple<string, Token>(inp[5..], new Token(TokenKind.Named, "named")),
|
||||||
|
_ when inp.StartsWith("parented_to ") => new Tuple<string, Token>(inp[11..], new Token(TokenKind.ParentedTo, "parented_to")),
|
||||||
|
_ when inp.StartsWith("prototyped ") => new Tuple<string, Token>(inp[10..], new Token(TokenKind.Prototyped, "prototyped")),
|
||||||
|
_ when inp.StartsWith("tagged ") => new Tuple<string, Token>(inp[6..], new Token(TokenKind.Tagged, "tagged")),
|
||||||
|
_ when inp.StartsWith("do ") => new Tuple<string, Token>(inp[2..], new Token(TokenKind.Do, "do")),
|
||||||
|
_ => ExtractStringToken(inp)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Tuple<string, Token> ExtractStringToken(string inp)
|
||||||
|
{
|
||||||
|
inp = inp.TrimStart();
|
||||||
|
if (inp.StartsWith("\""))
|
||||||
|
{
|
||||||
|
var acc = "";
|
||||||
|
var skipNext = false;
|
||||||
|
foreach (var rune in inp[1..])
|
||||||
|
{
|
||||||
|
if (skipNext)
|
||||||
|
{
|
||||||
|
acc += rune;
|
||||||
|
skipNext = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (rune)
|
||||||
|
{
|
||||||
|
case '\\':
|
||||||
|
skipNext = true;
|
||||||
|
continue;
|
||||||
|
case '"':
|
||||||
|
return new Tuple<string, Token>(inp[(acc.Length+2)..], new Token(TokenKind.String, acc));
|
||||||
|
default:
|
||||||
|
acc += rune;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Missing a \" somewhere.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inp.Contains(" ") == false)
|
||||||
|
{
|
||||||
|
return new Tuple<string, Token>("", new Token(TokenKind.String, inp));
|
||||||
|
}
|
||||||
|
var word = inp[..inp.IndexOf(" ", StringComparison.Ordinal)];
|
||||||
|
var rem = inp[inp.IndexOf(" ", StringComparison.Ordinal)..];
|
||||||
|
return new Tuple<string, Token>(rem, new Token(TokenKind.String, word));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts and evaluates a query, then returns the rest.
|
||||||
|
public static Tuple<string, IEnumerable<IEntity>> DoEntityQuery(string query, IEntityManager entityManager)
|
||||||
|
{
|
||||||
|
var remainingQuery = query;
|
||||||
|
var componentFactory = IoCManager.Resolve<IComponentFactory>();
|
||||||
|
var entities = entityManager.GetEntities();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
Token t;
|
||||||
|
(remainingQuery, t) = Token.ExtractOneToken(remainingQuery);
|
||||||
|
|
||||||
|
switch (t.Kind)
|
||||||
|
{
|
||||||
|
case TokenKind.With:
|
||||||
|
{
|
||||||
|
Token nt;
|
||||||
|
(remainingQuery, nt) = Token.ExtractOneToken(remainingQuery);
|
||||||
|
var comp = componentFactory.GetRegistration(nt.Text).Type;
|
||||||
|
entities = entities.Where(e => e.HasComponent(comp));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TokenKind.Named:
|
||||||
|
{
|
||||||
|
Token nt;
|
||||||
|
(remainingQuery, nt) = Token.ExtractOneToken(remainingQuery);
|
||||||
|
var r = new Regex("^" + nt.Text + "$");
|
||||||
|
entities = entities.Where(e => r.IsMatch(e.Name));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TokenKind.Tagged:
|
||||||
|
{
|
||||||
|
Token nt;
|
||||||
|
(remainingQuery, nt) = Token.ExtractOneToken(remainingQuery);
|
||||||
|
var text = nt.Text;
|
||||||
|
entities = entities.Where(e =>
|
||||||
|
{
|
||||||
|
if (e.TryGetComponent<TagComponent>(out var tagComponent))
|
||||||
|
{
|
||||||
|
return tagComponent.Tags.Contains(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TokenKind.ParentedTo:
|
||||||
|
{
|
||||||
|
Token nt;
|
||||||
|
(remainingQuery, nt) = Token.ExtractOneToken(remainingQuery);
|
||||||
|
var uid = EntityUid.Parse(nt.Text);
|
||||||
|
entities = entities.Where(e => e.Transform.Parent?.Owner.Uid == uid);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TokenKind.Prototyped:
|
||||||
|
{
|
||||||
|
Token nt;
|
||||||
|
(remainingQuery, nt) = Token.ExtractOneToken(remainingQuery);
|
||||||
|
entities = entities.Where(e => e.Prototype?.ID == nt.Text);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TokenKind.Do:
|
||||||
|
return new Tuple<string, IEnumerable<IEntity>>(remainingQuery, entities);
|
||||||
|
default:
|
||||||
|
throw new Exception("Unknown token called " + t.Text + ", which was parsed as a "+ t.Kind.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingQuery.TrimStart() == "")
|
||||||
|
return new Tuple<string, IEnumerable<IEntity>>(remainingQuery, entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Content.Server/Administration/Commands/BQL/ForAllCommand.cs
Normal file
38
Content.Server/Administration/Commands/BQL/ForAllCommand.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Content.Server.Commands;
|
||||||
|
using Content.Shared.Administration;
|
||||||
|
using Robust.Shared.Console;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
|
||||||
|
namespace Content.Server.Administration.Commands.BQL
|
||||||
|
{
|
||||||
|
[AdminCommand(AdminFlags.Admin)]
|
||||||
|
public class ForAllCommand : IConsoleCommand
|
||||||
|
{
|
||||||
|
public string Command => "forall";
|
||||||
|
public string Description => "Runs a command over all entities with a given component";
|
||||||
|
public string Help => "Usage: forall <comp> <command...>";
|
||||||
|
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length < 2)
|
||||||
|
{
|
||||||
|
shell.WriteLine(Help);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||||
|
var (command, entities) = BqlParser.DoEntityQuery(argStr[6..], entityManager);
|
||||||
|
|
||||||
|
foreach (var ent in entities.ToList())
|
||||||
|
{
|
||||||
|
var cmds = CommandUtils.SubstituteEntityDetails(shell, ent, command).Split(";");
|
||||||
|
foreach (var cmd in cmds)
|
||||||
|
{
|
||||||
|
shell.ExecuteCommand(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Globalization;
|
||||||
using Robust.Server.Player;
|
using Robust.Server.Player;
|
||||||
using Robust.Shared.Console;
|
using Robust.Shared.Console;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
@@ -51,5 +52,38 @@ namespace Content.Server.Commands
|
|||||||
attachedEntity = session.AttachedEntity;
|
attachedEntity = session.AttachedEntity;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string SubstituteEntityDetails(IConsoleShell shell, IEntity ent, string ruleString)
|
||||||
|
{
|
||||||
|
// gross, is there a better way to do this?
|
||||||
|
ruleString = ruleString.Replace("$ID", ent.Uid.ToString());
|
||||||
|
ruleString = ruleString.Replace("$WX",
|
||||||
|
ent.Transform.WorldPosition.X.ToString(CultureInfo.InvariantCulture));
|
||||||
|
ruleString = ruleString.Replace("$WY",
|
||||||
|
ent.Transform.WorldPosition.Y.ToString(CultureInfo.InvariantCulture));
|
||||||
|
ruleString = ruleString.Replace("$LX",
|
||||||
|
ent.Transform.LocalPosition.X.ToString(CultureInfo.InvariantCulture));
|
||||||
|
ruleString = ruleString.Replace("$LY",
|
||||||
|
ent.Transform.LocalPosition.Y.ToString(CultureInfo.InvariantCulture));
|
||||||
|
ruleString = ruleString.Replace("$NAME", ent.Name);
|
||||||
|
|
||||||
|
if (shell.Player is IPlayerSession player)
|
||||||
|
{
|
||||||
|
if (player.AttachedEntity != null)
|
||||||
|
{
|
||||||
|
var p = player.AttachedEntity;
|
||||||
|
ruleString = ruleString.Replace("$PID", ent.Uid.ToString());
|
||||||
|
ruleString = ruleString.Replace("$PWX",
|
||||||
|
p.Transform.WorldPosition.X.ToString(CultureInfo.InvariantCulture));
|
||||||
|
ruleString = ruleString.Replace("$PWY",
|
||||||
|
p.Transform.WorldPosition.Y.ToString(CultureInfo.InvariantCulture));
|
||||||
|
ruleString = ruleString.Replace("$PLX",
|
||||||
|
p.Transform.LocalPosition.X.ToString(CultureInfo.InvariantCulture));
|
||||||
|
ruleString = ruleString.Replace("$PLY",
|
||||||
|
p.Transform.LocalPosition.Y.ToString(CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ruleString;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user