osb/source/game/StarCommandProcessor.cpp

1025 lines
38 KiB
C++
Raw Normal View History

2023-06-20 04:33:09 +00:00
#include "StarCommandProcessor.hpp"
#include "StarLexicalCast.hpp"
#include "StarJsonExtra.hpp"
#include "StarNpc.hpp"
#include "StarWorldServer.hpp"
#include "StarUniverseServer.hpp"
#include "StarUniverseSettings.hpp"
#include "StarRoot.hpp"
#include "StarItemDatabase.hpp"
#include "StarConfiguration.hpp"
#include "StarItemDrop.hpp"
#include "StarTreasure.hpp"
#include "StarLogging.hpp"
#include "StarPlayer.hpp"
#include "StarMonster.hpp"
#include "StarStagehand.hpp"
#include "StarVehicleDatabase.hpp"
#include "StarStagehandDatabase.hpp"
#include "StarLiquidsDatabase.hpp"
#include "StarChatProcessor.hpp"
#include "StarAssets.hpp"
#include "StarWorldLuaBindings.hpp"
#include "StarUniverseServerLuaBindings.hpp"
namespace Star {
CommandProcessor::CommandProcessor(UniverseServer* universe)
: m_universe(universe) {
auto assets = Root::singleton().assets();
m_scriptComponent.addCallbacks("universe", LuaBindings::makeUniverseServerCallbacks(m_universe));
m_scriptComponent.addCallbacks("CommandProcessor", makeCommandCallbacks());
m_scriptComponent.setScripts(jsonToStringList(assets->json("/universe_server.config:commandProcessorScripts")));
auto luaRoot = make_shared<LuaRoot>();
luaRoot->luaEngine().setNullTerminated(false);
m_scriptComponent.setLuaRoot(luaRoot);
2023-06-20 04:33:09 +00:00
m_scriptComponent.init();
}
String CommandProcessor::adminCommand(String const& command, String const& argumentString) {
MutexLocker locker(m_mutex);
return handleCommand(ServerConnectionId, command, argumentString);
}
String CommandProcessor::userCommand(ConnectionId connectionId, String const& command, String const& argumentString) {
MutexLocker locker(m_mutex);
if (connectionId == ServerConnectionId)
throw StarException("CommandProcessor::userCommand called with ServerConnectionId");
return handleCommand(connectionId, command, argumentString);
}
String CommandProcessor::help(ConnectionId connectionId, String const& argumentString) {
auto arguments = m_parser.tokenizeToStringList(argumentString);
auto assets = Root::singleton().assets();
auto basicCommands = assets->json("/help.config:basicCommands");
auto adminCommands = assets->json("/help.config:adminCommands");
auto debugCommands = assets->json("/help.config:debugCommands");
if (arguments.size()) {
if (arguments.size() >= 1) {
if (auto helpText = basicCommands.optString(arguments[0]).orMaybe(adminCommands.optString(arguments[0])).orMaybe(debugCommands.optString(arguments[0])))
return *helpText;
}
}
String res = "";
auto commandDescriptions = [&](Json const& commandConfig) {
StringList commandList = commandConfig.toObject().keys();
sort(commandList);
return "/" + commandList.join(", /");
};
String basicHelpFormat = assets->json("/help.config:basicHelpText").toString();
res = res + strf(basicHelpFormat.utf8Ptr(), commandDescriptions(basicCommands));
if (!adminCheck(connectionId, "")) {
String adminHelpFormat = assets->json("/help.config:adminHelpText").toString();
res = res + "\n" + strf(adminHelpFormat.utf8Ptr(), commandDescriptions(adminCommands));
String debugHelpFormat = assets->json("/help.config:debugHelpText").toString();
res = res + "\n" + strf(debugHelpFormat.utf8Ptr(), commandDescriptions(debugCommands));
}
res = res + "\n" + basicCommands.getString("help");
return res;
}
String CommandProcessor::admin(ConnectionId connectionId, String const&) {
auto config = Root::singleton().configuration();
if (m_universe->canBecomeAdmin(connectionId)) {
if (connectionId == ServerConnectionId)
return "Invalid client state";
if (!config->get("allowAdminCommands").toBool())
return "Admin commands disabled on this server.";
bool wasAdmin = m_universe->isAdmin(connectionId);
m_universe->setAdmin(connectionId, !wasAdmin);
if (!wasAdmin)
2023-06-27 10:23:44 +00:00
return strf("Admin privileges now given to player {}", m_universe->clientNick(connectionId));
2023-06-20 04:33:09 +00:00
else
2023-06-27 10:23:44 +00:00
return strf("Admin privileges taken away from {}", m_universe->clientNick(connectionId));
2023-06-20 04:33:09 +00:00
} else {
return "Insufficient privileges to make self admin.";
}
}
String CommandProcessor::pvp(ConnectionId connectionId, String const&) {
if (!m_universe->isPvp(connectionId)) {
m_universe->setPvp(connectionId, true);
if (m_universe->isPvp(connectionId))
2023-06-27 10:23:44 +00:00
m_universe->adminBroadcast(strf("Player {} is now PVP", m_universe->clientNick(connectionId)));
2023-06-20 04:33:09 +00:00
} else {
m_universe->setPvp(connectionId, false);
if (!m_universe->isPvp(connectionId))
2023-06-27 10:23:44 +00:00
m_universe->adminBroadcast(strf("Player {} is a big wimp and is no longer PVP", m_universe->clientNick(connectionId)));
2023-06-20 04:33:09 +00:00
}
if (m_universe->isPvp(connectionId))
return "PVP active";
else
return "PVP inactive";
}
String CommandProcessor::whoami(ConnectionId connectionId, String const&) {
2023-06-27 10:23:44 +00:00
return strf("Server: You are {}. You are {}an Admin",
2023-06-20 04:33:09 +00:00
m_universe->clientNick(connectionId),
m_universe->isAdmin(connectionId) ? "" : "not ");
}
String CommandProcessor::warp(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "do the space warp again"))
return *errorMsg;
try {
m_universe->clientWarpPlayer(connectionId, parseWarpAction(argumentString));
return "Lets do the space warp again";
} catch (StarException const& e) {
2023-06-27 10:23:44 +00:00
Logger::warn("Could not parse warp target: {}", outputException(e, false));
return strf("Could not parse the argument {} as a warp target", argumentString);
2023-06-20 04:33:09 +00:00
}
}
String CommandProcessor::warpRandom(ConnectionId connectionId, String const& typeName) {
if (auto errorMsg = adminCheck(connectionId, "warp to random world"))
return *errorMsg;
Vec2I size = {2, 2};
auto& celestialDatabase = m_universe->celestialDatabase();
Maybe<CelestialCoordinate> target = {};
auto validPlanet = [&celestialDatabase, &typeName](CelestialCoordinate const& p) {
if (auto celestialParams = celestialDatabase.parameters(p)) {
if (auto visitableParams = celestialParams->visitableParameters()) {
if (visitableParams->typeName == typeName)
return true;
}
}
return false;
};
while (target.isNothing()) {
RectI region = RectI::withSize(Vec2I(Random::randi32(), Random::randi32()), size);
while (!celestialDatabase.scanRegionFullyLoaded(region)) {
celestialDatabase.scanSystems(region);
}
auto systems = celestialDatabase.scanSystems(region);
for (auto s : systems) {
for (auto planet : celestialDatabase.children(s)) {
if (validPlanet(planet))
target = planet;
if (target.isNothing()) {
for (auto moon : celestialDatabase.children(planet)) {
if (validPlanet(moon)) {
target = moon;
break;
}
}
}
}
}
2023-06-20 04:33:09 +00:00
if (size.magnitude() > 1024)
return "could not find a matching world";
size *= 2;
}
m_universe->clientWarpPlayer(connectionId, WarpToWorld(CelestialWorldId(*target)));
2023-06-27 10:23:44 +00:00
return strf("warping to {}", *target);
2023-06-20 04:33:09 +00:00
}
String CommandProcessor::timewarp(ConnectionId connectionId, String const& argumentsString) {
2023-06-20 04:33:09 +00:00
if (auto errorMsg = adminCheck(connectionId, "do the time warp again"))
return *errorMsg;
auto arguments = m_parser.tokenizeToStringList(argumentsString);
if (arguments.empty())
return "Not enough arguments to /timewarp";
2023-06-20 04:33:09 +00:00
try {
auto time = lexicalCast<double>(arguments.at(0));
if (time == 0.0)
return "You suck at time travel.";
else if (time < 0.0 && (arguments.size() < 2 || arguments[1] != "please"))
2023-06-20 04:33:09 +00:00
return "Great Scott! We can't go back in time!";
m_universe->universeClock()->adjustTime(time);
return time > 0.0 ? "It's just a jump to the left..." : "And then a step to the right...";
2023-06-20 04:33:09 +00:00
} catch (BadLexicalCast const&) {
return strf("Could not parse the argument {} as a time adjustment", arguments[0]);
2023-06-20 04:33:09 +00:00
}
}
String CommandProcessor::timescale(ConnectionId connectionId, String const& argumentsString) {
if (auto errorMsg = adminCheck(connectionId, "mess with time"))
return *errorMsg;
auto arguments = m_parser.tokenizeToStringList(argumentsString);
if (arguments.empty())
2024-03-19 03:44:21 +00:00
return strf("Current timescale is {:6.6f}x", GlobalTimescale);
float timescale = clamp(lexicalCast<float>(arguments[0]), 0.001f, 32.0f);
m_universe->setTimescale(timescale);
return strf("Set timescale to {:6.6f}x", timescale);
}
String CommandProcessor::tickrate(ConnectionId connectionId, String const& argumentsString) {
if (auto errorMsg = adminCheck(connectionId, "change the tick rate"))
return *errorMsg;
auto arguments = m_parser.tokenizeToStringList(argumentsString);
if (arguments.empty())
2024-03-19 03:44:21 +00:00
return strf("Current tick rate is {:4.2f}Hz", 1.0f / ServerGlobalTimestep);
2024-03-19 03:44:21 +00:00
float tickRate = clamp(lexicalCast<float>(arguments[0]), 5.f, 500.f);
m_universe->setTickRate(tickRate);
2024-03-19 03:44:21 +00:00
return strf("Set tick rate to {:4.2f}Hz", tickRate);
}
2023-06-20 04:33:09 +00:00
String CommandProcessor::setTileProtection(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "modify world properties")) {
return *errorMsg;
}
auto arguments = m_parser.tokenizeToStringList(argumentString);
if (arguments.size() < 2)
return "Not enough arguments to /settileprotection. Use /settileprotection <dungeonId> <protected>";
try {
DungeonId dungeonId = lexicalCast<DungeonId>(arguments.at(0));
bool isProtected = lexicalCast<bool>(arguments.at(1));
bool done = m_universe->executeForClient(connectionId, [dungeonId, isProtected](WorldServer* world, PlayerPtr const&) {
world->setTileProtection(dungeonId, isProtected);
});
return done ? "" : "Failed to set block protection.";
} catch (BadLexicalCast const&) {
return strf("Could not parse /settileprotection parameters. Use /settileprotection <dungeonId> <protected>", argumentString);
}
}
String CommandProcessor::setDungeonId(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "set dungeon id")) {
return *errorMsg;
}
auto arguments = m_parser.tokenizeToStringList(argumentString);
if (arguments.size() < 1)
return "Not enough arguments to /setdungeonid. Use /setdungeonid <dungeonId>";
try {
DungeonId dungeonId = lexicalCast<DungeonId>(arguments.at(0));
bool done = m_universe->executeForClient(connectionId, [dungeonId](WorldServer* world, PlayerPtr const& player) {
world->setDungeonId(RectI::withSize(Vec2I(player->aimPosition()), Vec2I(1, 1)), dungeonId);
});
return done ? "" : "Failed to set dungeon id.";
} catch (BadLexicalCast const&) {
return strf("Could not parse /setdungeonid parameters. Use /setdungeonid <dungeonId>!", argumentString);
}
}
String CommandProcessor::setPlayerStart(ConnectionId connectionId, String const&) {
if (auto errorMsg = adminCheck(connectionId, "modify world properties"))
return *errorMsg;
m_universe->executeForClient(connectionId, [](WorldServer* world, PlayerPtr const& player) {
world->setPlayerStart(player->position() + player->feetOffset());
});
return "";
}
String CommandProcessor::spawnItem(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "spawn items"))
return *errorMsg;
auto arguments = m_parser.tokenizeToStringList(argumentString);
if (arguments.empty())
2023-06-20 04:33:09 +00:00
return "Not enough arguments to /spawnitem";
try {
String kind = arguments.at(0);
Json parameters = JsonObject();
unsigned amount = 1;
Maybe<float> level;
Maybe<uint64_t> seed;
if (arguments.size() >= 2)
amount = lexicalCast<unsigned>(arguments.at(1));
if (arguments.size() >= 3)
parameters = Json::parse(arguments.at(2));
if (arguments.size() >= 4)
level = lexicalCast<float>(arguments.at(3));
if (arguments.size() >= 5)
seed = lexicalCast<uint64_t>(arguments.at(4));
bool done = m_universe->executeForClient(connectionId, [&](WorldServer* world, PlayerPtr const& player) {
auto itemDatabase = Root::singleton().itemDatabase();
world->addEntity(ItemDrop::createRandomizedDrop(itemDatabase->item(ItemDescriptor(kind, amount, parameters), level, seed, true), player->aimPosition()));
2023-06-20 04:33:09 +00:00
});
return done ? "" : "Invalid client state";
} catch (JsonParsingException const& exception) {
2023-06-27 10:23:44 +00:00
Logger::warn("Error while processing /spawnitem '{}' command. Json parse problem: {}", arguments.at(0), outputException(exception, false));
2023-06-20 04:33:09 +00:00
return "Could not parse item parameters";
} catch (ItemException const& exception) {
2023-06-27 10:23:44 +00:00
Logger::warn("Error while processing /spawnitem '{}' command. Item instantiation problem: {}", arguments.at(0), outputException(exception, false));
return strf("Could not load item '{}'", arguments.at(0));
2023-06-20 04:33:09 +00:00
} catch (BadLexicalCast const& exception) {
2023-06-27 10:23:44 +00:00
Logger::warn("Error while processing /spawnitem command. Number expected. Got something else: {}", outputException(exception, false));
return strf("Could not load item '{}'", arguments.at(0));
2023-06-20 04:33:09 +00:00
} catch (StarException const& exception) {
2023-06-27 10:23:44 +00:00
Logger::warn("Error while processing /spawnitem command '{}', exception caught: {}", argumentString, outputException(exception, false));
return strf("Could not load item '{}'", arguments.at(0));
2023-06-20 04:33:09 +00:00
}
}
String CommandProcessor::spawnTreasure(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "spawn items"))
return *errorMsg;
auto arguments = m_parser.tokenizeToStringList(argumentString);
if (arguments.empty())
2023-06-20 04:33:09 +00:00
return "Not enough arguments to /spawntreasure";
try {
String treasurePool = arguments.at(0);
unsigned level = 1;
if (arguments.size() >= 2)
level = lexicalCast<unsigned>(arguments.at(1));
bool done = m_universe->executeForClient(connectionId, [&](WorldServer* world, PlayerPtr const& player) {
auto treasureDatabase = Root::singleton().treasureDatabase();
for (auto const& treasureItem : treasureDatabase->createTreasure(treasurePool, level, Random::randu64()))
world->addEntity(ItemDrop::createRandomizedDrop(treasureItem, player->aimPosition()));
});
return done ? "" : "Invalid client state";
} catch (JsonParsingException const& exception) {
2023-06-27 10:23:44 +00:00
Logger::warn("Error while processing /spawntreasure '{}' command. Json parse problem: {}", arguments.at(0), outputException(exception, false));
2023-06-20 04:33:09 +00:00
return "Could not parse item parameters";
} catch (ItemException const& exception) {
2023-06-27 10:23:44 +00:00
Logger::warn("Error while processing /spawntreasure '{}' command. Item instantiation problem: {}", arguments.at(0), outputException(exception, false));
return strf("Could not load item '{}'", arguments.at(0));
2023-06-20 04:33:09 +00:00
} catch (BadLexicalCast const& exception) {
2023-06-27 10:23:44 +00:00
Logger::warn("Error while processing /spawntreasure command. Number expected. Got something else: {}", outputException(exception, false));
return strf("Could not load item '{}'", arguments.at(0));
2023-06-20 04:33:09 +00:00
} catch (StarException const& exception) {
2023-06-27 10:23:44 +00:00
Logger::warn("Error while processing /spawntreasure command '{}', exception caught: {}", argumentString, outputException(exception, false));
return strf("Could not load item '{}'", arguments.at(0));
2023-06-20 04:33:09 +00:00
}
}
String CommandProcessor::spawnMonster(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "spawn monsters"))
return *errorMsg;
try {
auto arguments = m_parser.tokenizeToStringList(argumentString);
auto monsterDatabase = Root::singleton().monsterDatabase();
MonsterPtr monster;
float level = 1;
if (arguments.size() >= 2)
level = lexicalCast<float>(arguments.at(1));
Json parameters = JsonObject();
if (arguments.size() >= 3)
parameters = parameters.setAll(Json::parse(arguments.at(2)).toObject());
monster = monsterDatabase->createMonster(monsterDatabase->randomMonster(arguments.at(0), parameters.toObject()), level);
bool done = m_universe->executeForClient(connectionId,
[&](WorldServer* world, PlayerPtr const& player) {
monster->setPosition(player->aimPosition());
world->addEntity(monster);
});
return done ? "" : "Invalid client state";
} catch (StarException const& exception) {
2023-06-27 10:23:44 +00:00
Logger::warn("Could not spawn Monster of type '{}', exception caught: {}", argumentString, outputException(exception, false));
return strf("Could not spawn Monster of type '{}'", argumentString);
2023-06-20 04:33:09 +00:00
}
}
String CommandProcessor::spawnNpc(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "spawn NPCs"))
return *errorMsg;
auto arguments = m_parser.tokenizeToStringList(argumentString);
try {
auto npcDatabase = Root::singleton().npcDatabase();
float npcLevel = 1;
uint64_t seed = Random::randu64();
Json overrides;
if (arguments.size() < 2)
return "You must specify a species and NPC type to spawn.";
if (arguments.size() >= 3)
npcLevel = lexicalCast<float>(arguments.at(2));
if (arguments.size() >= 4)
seed = lexicalCast<uint64_t>(arguments.at(3));
if (arguments.size() >= 5)
overrides = Json::parse(arguments.at(4)).toObject();
auto npc = npcDatabase->createNpc(npcDatabase->generateNpcVariant(arguments.at(0), arguments.at(1), npcLevel, seed, overrides));
bool done = m_universe->executeForClient(connectionId, [&](WorldServer* world, PlayerPtr const& player) {
npc->setPosition(player->aimPosition());
world->addEntity(npc);
});
return done ? "" : "Invalid client state";
} catch (StarException const& exception) {
2023-06-27 10:23:44 +00:00
Logger::warn("Could not spawn NPC of species '{}', exception caught: {}", argumentString, outputException(exception, true));
return strf("Could not spawn NPC of species '{}'", argumentString);
2023-06-20 04:33:09 +00:00
}
}
String CommandProcessor::spawnVehicle(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "spawn vehicles"))
return *errorMsg;
try {
auto vehicleDatabase = Root::singleton().vehicleDatabase();
auto arguments = m_parser.tokenizeToStringList(argumentString);
VehiclePtr vehicle;
String name = arguments.at(0);
Json parameters = JsonObject();
if (arguments.size() >= 2)
parameters = Json::parse(arguments.at(1)).toObject();
vehicle = vehicleDatabase->create(name, parameters);
bool done = m_universe->executeForClient(connectionId,
[&](WorldServer* world, PlayerPtr const& player) {
vehicle->setPosition(player->aimPosition());
world->addEntity(std::move(vehicle));
2023-06-20 04:33:09 +00:00
});
return done ? "" : "Invalid client state";
} catch (StarException const& exception) {
2023-06-27 10:23:44 +00:00
Logger::warn("Could not spawn vehicle, exception caught: {}", outputException(exception, false));
2023-06-20 04:33:09 +00:00
return strf("Could not spawn vehicle");
}
}
String CommandProcessor::spawnStagehand(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "spawn stagehands"))
return *errorMsg;
try {
auto arguments = m_parser.tokenizeToStringList(argumentString);
auto stagehandDatabase = Root::singleton().stagehandDatabase();
Json parameters = JsonObject();
if (arguments.size() >= 2)
parameters = Json::parse(arguments.at(1)).toObject();
auto stagehand = stagehandDatabase->createStagehand(arguments.at(0), parameters);
bool done = m_universe->executeForClient(connectionId, [&](WorldServer* world, PlayerPtr player) {
stagehand->setPosition(player->aimPosition());
world->addEntity(stagehand);
});
return done ? "" : "Invalid client state";
} catch (StarException const& exception) {
2023-06-27 10:23:44 +00:00
Logger::warn("Could not spawn Stagehand of type '{}', exception caught: {}", argumentString, outputException(exception, false));
return strf("Could not spawn Stagehand of type '{}'", argumentString);
2023-06-20 04:33:09 +00:00
}
}
String CommandProcessor::clearStagehand(ConnectionId connectionId, String const&) {
if (auto errorMsg = adminCheck(connectionId, "remove stagehands"))
return *errorMsg;
unsigned removed = 0;
bool done = m_universe->executeForClient(connectionId,
[&](WorldServer* world, PlayerPtr player) {
auto queryRect = RectF::withCenter(player->aimPosition(), Vec2F{2, 2});
for (auto stagehand : world->query<Stagehand>(queryRect)) {
world->removeEntity(stagehand->entityId(), true);
++removed;
}
});
2023-06-27 10:23:44 +00:00
return done ? strf("Removed {} stagehands", removed) : "Invalid client state";
2023-06-20 04:33:09 +00:00
}
String CommandProcessor::spawnLiquid(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "spawn liquid"))
return *errorMsg;
try {
auto arguments = m_parser.tokenizeToStringList(argumentString);
auto liquidsDatabase = Root::singleton().liquidsDatabase();
if (!liquidsDatabase->isLiquidName(arguments.at(0)))
2023-06-27 10:23:44 +00:00
return strf("No such liquid {}", arguments.at(0));
2023-06-20 04:33:09 +00:00
LiquidId liquid = liquidsDatabase->liquidId(arguments.at(0));
float quantity = 1.0f;
if (arguments.size() > 1) {
if (auto maybeQuantity = maybeLexicalCast<float>(arguments.at(1)))
quantity = *maybeQuantity;
else
2023-06-27 10:23:44 +00:00
return strf("Could not parse quantity value '{}'", arguments.at(1));
2023-06-20 04:33:09 +00:00
}
bool done = m_universe->executeForClient(connectionId, [&](WorldServer* world, PlayerPtr const& player) {
world->modifyTile(Vec2I(player->aimPosition().floor()), PlaceLiquid{liquid, quantity}, true);
});
return done ? "" : "Invalid client state";
} catch (StarException const& exception) {
Logger::warn(
2023-06-27 10:23:44 +00:00
"Could not spawn liquid '{}', exception caught: {}", argumentString, outputException(exception, false));
2023-06-20 04:33:09 +00:00
return "Could not spawn liquid.";
}
}
String CommandProcessor::kick(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "kick a user"))
return *errorMsg;
auto arguments = m_parser.tokenizeToStringList(argumentString);
if (arguments.empty())
2023-06-20 04:33:09 +00:00
return "No player specified";
auto toKick = playerCidFromCommand(arguments[0], m_universe);
if (!toKick)
2023-06-27 10:23:44 +00:00
return strf("No user with specifier {} found.", arguments[0]);
2023-06-20 04:33:09 +00:00
// Like IRC, if only the nick is passed then the nick is used as the reason
if (arguments.size() == 1)
arguments.append(m_universe->clientNick(*toKick));
m_universe->disconnectClient(*toKick, arguments[1]);
2023-06-27 10:23:44 +00:00
return strf("Successfully kicked user with specifier {}. ConnectionId: {}. Reason given: {}",
2023-06-20 04:33:09 +00:00
arguments[0],
toKick,
arguments[1]);
}
String CommandProcessor::ban(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "ban a user"))
return *errorMsg;
auto arguments = m_parser.tokenizeToStringList(argumentString);
if (arguments.empty())
2023-06-20 04:33:09 +00:00
return "No player specified";
auto toKick = playerCidFromCommand(arguments[0], m_universe);
if (!toKick)
2023-06-27 10:23:44 +00:00
return strf("No user with specifier {} found.", arguments[0]);
2023-06-20 04:33:09 +00:00
String reason = arguments[0];
if (arguments.size() < 2)
reason = m_universe->clientNick(*toKick);
else
reason = arguments[1];
pair<bool, bool> type = {true, true};
if (arguments.size() >= 3) {
if (arguments[2] == "ip") {
type = {true, false};
} else if (arguments[2] == "uuid") {
type = {false, true};
} else if (arguments[2] == "both") {
type = {true, true};
} else {
2023-06-27 10:23:44 +00:00
return strf("Invalid argument {} passed as ban type to /ban. Options are ip, uuid, or both.", arguments[2]);
2023-06-20 04:33:09 +00:00
}
}
Maybe<int> banTime;
if (arguments.size() == 4) {
try {
banTime = lexicalCast<int>(arguments[3]);
} catch (BadLexicalCast const&) {
2023-06-27 10:23:44 +00:00
return strf("Invalid argument {} passed as ban time to /ban.", arguments[3]);
2023-06-20 04:33:09 +00:00
}
}
m_universe->banUser(*toKick, reason, type, banTime);
2023-06-27 10:23:44 +00:00
return strf("Successfully kicked user with specifier {}. ConnectionId: {}. Reason given: {}",
2023-06-20 04:33:09 +00:00
arguments[0], toKick, reason);
}
String CommandProcessor::unbanIp(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "unban a user"))
return *errorMsg;
auto arguments = m_parser.tokenizeToStringList(argumentString);
if (arguments.empty())
2023-06-20 04:33:09 +00:00
return "No IP specified";
bool success = m_universe->unbanIp(arguments[0]);
if (success)
2023-06-27 10:23:44 +00:00
return strf("Successfully removed IP {} from ban list", arguments[0]);
2023-06-20 04:33:09 +00:00
else
2023-06-27 10:23:44 +00:00
return strf("'{}' is not a valid IP or was not found in the bans list", arguments[0]);
2023-06-20 04:33:09 +00:00
}
String CommandProcessor::unbanUuid(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "unban a user"))
return *errorMsg;
auto arguments = m_parser.tokenizeToStringList(argumentString);
if (arguments.empty())
2023-06-20 04:33:09 +00:00
return "No UUID specified";
bool success = m_universe->unbanUuid(arguments[0]);
if (success)
2023-06-27 10:23:44 +00:00
return strf("Successfully removed UUID {} from ban list", arguments[0]);
2023-06-20 04:33:09 +00:00
else
2023-06-27 10:23:44 +00:00
return strf("'{}' is not a valid UUID or was not found in the bans list", arguments[0]);
2023-06-20 04:33:09 +00:00
}
String CommandProcessor::list(ConnectionId connectionId, String const&) {
if (auto errorMsg = adminCheck(connectionId, "list clients"))
return *errorMsg;
StringList res;
auto assets = Root::singleton().assets();
for (auto cid : m_universe->clientIds())
2023-06-27 10:23:44 +00:00
res.append(strf("${} : {} : $${}", cid, m_universe->clientNick(cid), m_universe->uuidForClient(cid)->hex()));
2023-06-20 04:33:09 +00:00
return res.join("\n");
}
String CommandProcessor::clientCoordinate(ConnectionId connectionId, String const& argumentString) {
ConnectionId targetClientId = connectionId;
String targetLabel = "Your";
auto arguments = m_parser.tokenizeToStringList(argumentString);
if (!adminCheck(connectionId, "find other players")) {
if (arguments.size() > 0) {
auto cid = playerCidFromCommand(arguments[0], m_universe);
if (!cid)
2023-06-27 10:23:44 +00:00
return strf("No user with specifier {} found.", arguments[0]);
2023-06-20 04:33:09 +00:00
targetClientId = *cid;
2023-06-27 10:23:44 +00:00
targetLabel = strf("Client {}'s", arguments[0]);
2023-06-20 04:33:09 +00:00
}
}
if (targetClientId) {
auto worldId = m_universe->clientWorld(targetClientId);
2023-06-27 10:23:44 +00:00
return strf("{} current location is {}", targetLabel, worldId);
2023-06-20 04:33:09 +00:00
} else {
return "";
}
}
String CommandProcessor::serverReload(ConnectionId connectionId, String const&) {
if (auto errorMsg = adminCheck(connectionId, "trigger root reload"))
return *errorMsg;
auto& root = Root::singleton();
root.reload();
root.fullyLoad();
return "";
}
String CommandProcessor::eval(ConnectionId connectionId, String const& lua) {
if (auto errorMsg = localCheck(connectionId, "execute server script"))
return *errorMsg;
if (auto errorMsg = adminCheck(connectionId, "execute server script"))
return *errorMsg;
return toString(m_scriptComponent.context()->eval(lua));
}
String CommandProcessor::entityEval(ConnectionId connectionId, String const& lua) {
if (auto errorMsg = localCheck(connectionId, "execute server entity script"))
return *errorMsg;
if (auto errorMsg = adminCheck(connectionId, "execute server entity script"))
return *errorMsg;
String message;
bool done = m_universe->executeForClient(connectionId,
[&lua, &message](WorldServer* world, PlayerPtr const& player) {
auto queryRect = RectF::withCenter(player->aimPosition(), Vec2F{2, 2});
auto entities = world->query<ScriptedEntity>(queryRect);
if (entities.empty()) {
message = "Could not find scripted entity at cursor";
return;
}
ScriptedEntityPtr targetEntity;
for (auto const& entity : entities) {
if (!targetEntity
|| vmagSquared(entity->position() - player->aimPosition())
< vmagSquared(targetEntity->position() - player->aimPosition()))
targetEntity = entity;
}
if (auto res = targetEntity->evalScript(lua))
message = toString(*res);
else
message = "Error evaluating script in entity context, check log";
});
return done ? message : "failed to do entity eval";
}
String CommandProcessor::enableSpawning(ConnectionId connectionId, String const&) {
if (auto errorMsg = adminCheck(connectionId, "enable world spawning"))
return *errorMsg;
bool done = m_universe->executeForClient(
connectionId, [](WorldServer* world, PlayerPtr const&) { world->setSpawningEnabled(true); });
return done ? "enabled monster spawning" : "enabling monster spawning failed";
}
String CommandProcessor::disableSpawning(ConnectionId connectionId, String const&) {
if (auto errorMsg = adminCheck(connectionId, "disable world spawning"))
return *errorMsg;
bool done = m_universe->executeForClient(
connectionId, [](WorldServer* world, PlayerPtr const&) { world->setSpawningEnabled(false); });
return done ? "disabled monster spawning" : "disabling monster spawning failed";
}
String CommandProcessor::placeDungeon(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "place dungeons"))
return *errorMsg;
auto arguments = m_parser.tokenizeToStringList(argumentString);
String dungeonName = arguments.at(0);
Maybe<Vec2I> targetPosition;
if (arguments.size() > 1) {
auto pos = arguments.at(1).split(",", 1);
targetPosition = Vec2I(lexicalCast<int>(pos.at(0)), lexicalCast<int>(pos.at(1)));
}
bool done = m_universe->executeForClient(connectionId,
[dungeonName, targetPosition](WorldServer* world, PlayerPtr const& player) {
world->placeDungeon(dungeonName, targetPosition.value(Vec2I::floor(player->aimPosition())), true);
});
return done ? "" : "Unable to place dungeon " + dungeonName;
}
String CommandProcessor::setUniverseFlag(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "set universe flags"))
return *errorMsg;
auto arguments = m_parser.tokenizeToStringList(argumentString);
String flag = arguments.at(0);
m_universe->universeSettings()->setFlag(flag);
return "set universe flag " + flag;
}
String CommandProcessor::resetUniverseFlags(ConnectionId connectionId, String const&) {
if (auto errorMsg = adminCheck(connectionId, "reset universe flags"))
return *errorMsg;
m_universe->universeSettings()->resetFlags();
return "universe flags reset!";
}
String CommandProcessor::addBiomeRegion(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "add biome regions"))
return *errorMsg;
auto arguments = m_parser.tokenizeToStringList(argumentString);
String biomeName = arguments.at(0);
int width = lexicalCast<int>(arguments.at(1));
String subBlockSelector = "largeClumps";
if (arguments.size() > 2)
subBlockSelector = arguments.at(2);
bool done = m_universe->executeForClient(connectionId,
[biomeName, width, subBlockSelector](WorldServer* world, PlayerPtr const& player) {
world->addBiomeRegion(Vec2I::floor(player->aimPosition()), biomeName, subBlockSelector, width);
});
2023-06-27 10:23:44 +00:00
return done ? strf("added region of biome {} with width {}", biomeName, width) : "failed to add biome region";
2023-06-20 04:33:09 +00:00
}
String CommandProcessor::expandBiomeRegion(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "expand biome regions"))
return *errorMsg;
auto arguments = m_parser.tokenizeToStringList(argumentString);
int newWidth = lexicalCast<int>(arguments.at(0));
bool done = m_universe->executeForClient(connectionId,
[newWidth](WorldServer* world, PlayerPtr const& player) {
world->expandBiomeRegion(Vec2I::floor(player->aimPosition()), newWidth);
});
2023-06-27 10:23:44 +00:00
return done ? strf("expanded region to width {}", newWidth) : "failed to expand biome region";
2023-06-20 04:33:09 +00:00
}
String CommandProcessor::updatePlanetType(ConnectionId connectionId, String const& argumentString) {
if (auto errorMsg = adminCheck(connectionId, "update planet type"))
return *errorMsg;
auto arguments = m_parser.tokenizeToStringList(argumentString);
auto coordinate = CelestialCoordinate(arguments.at(0));
auto newType = arguments.at(1);
auto weatherBiome = arguments.at(2);
bool done = m_universe->updatePlanetType(coordinate, newType, weatherBiome);
2023-06-27 10:23:44 +00:00
return done ? strf("set planet at {} to type {} weatherBiome {}", coordinate, newType, weatherBiome) : "failed to update planet type";
2023-06-20 04:33:09 +00:00
}
String CommandProcessor::setEnvironmentBiome(ConnectionId connectionId, String const&) {
if (auto errorMsg = adminCheck(connectionId, "update layer environment biome"))
return *errorMsg;
bool done = m_universe->executeForClient(connectionId,
[](WorldServer* world, PlayerPtr const& player) {
world->setLayerEnvironmentBiome(Vec2I::floor(player->aimPosition()));
});
return done ? "set environment biome for world layer" : "failed to set environment biome";
}
Maybe<ConnectionId> CommandProcessor::playerCidFromCommand(String const& player, UniverseServer* universe) {
char const* const UsernamePrefix = "@";
char const* const CidPrefix = "$";
char const* const UUIDPrefix = "$$";
if (player.beginsWith(UsernamePrefix)) {
return universe->findNick(player.substr(strlen(UsernamePrefix)));
} else if (player.beginsWith(UUIDPrefix)) {
try {
auto uuidString = player.substr(strlen(UUIDPrefix));
return universe->clientForUuid(Uuid(uuidString));
} catch (UuidException const&) {
// pass to base case
}
} else if (player.beginsWith(CidPrefix)) {
auto cidString = player.substr(strlen(CidPrefix));
auto cid = maybeLexicalCast<ConnectionId>(cidString).value(ServerConnectionId);
if (universe->isConnectedClient(cid))
return cid;
}
return universe->findNick(player);
}
//wow, wtf. TODO: replace with hashmap
2023-06-20 04:33:09 +00:00
String CommandProcessor::handleCommand(ConnectionId connectionId, String const& command, String const& argumentString) {
if (command == "admin") {
return admin(connectionId, argumentString);
} else if (command == "timewarp") {
return timewarp(connectionId, argumentString);
} else if (command == "timescale") {
return timescale(connectionId, argumentString);
} else if (command == "tickrate") {
return tickrate(connectionId, argumentString);
2023-06-20 04:33:09 +00:00
} else if (command == "settileprotection") {
return setTileProtection(connectionId, argumentString);
} else if (command == "setdungeonid") {
return setDungeonId(connectionId, argumentString);
} else if (command == "setspawnpoint") {
return setPlayerStart(connectionId, argumentString);
} else if (command == "spawnitem") {
return spawnItem(connectionId, argumentString);
} else if (command == "spawntreasure") {
return spawnTreasure(connectionId, argumentString);
} else if (command == "spawnmonster") {
return spawnMonster(connectionId, argumentString);
} else if (command == "spawnnpc") {
return spawnNpc(connectionId, argumentString);
} else if (command == "spawnstagehand") {
return spawnStagehand(connectionId, argumentString);
} else if (command == "clearstagehand") {
return clearStagehand(connectionId, argumentString);
} else if (command == "spawnvehicle") {
return spawnVehicle(connectionId, argumentString);
} else if (command == "spawnliquid") {
return spawnLiquid(connectionId, argumentString);
} else if (command == "pvp") {
return pvp(connectionId, argumentString);
} else if (command == "serverwhoami") {
return whoami(connectionId, argumentString);
} else if (command == "kick") {
return kick(connectionId, argumentString);
} else if (command == "ban") {
return ban(connectionId, argumentString);
} else if (command == "unbanip") {
return unbanIp(connectionId, argumentString);
} else if (command == "unbanuuid") {
return unbanUuid(connectionId, argumentString);
} else if (command == "list") {
return list(connectionId, argumentString);
} else if (command == "help") {
return help(connectionId, argumentString);
} else if (command == "warp") {
return warp(connectionId, argumentString);
} else if (command == "warprandom") {
return warpRandom(connectionId, argumentString);
} else if (command == "whereami") {
return clientCoordinate(connectionId, argumentString);
} else if (command == "whereis") {
return clientCoordinate(connectionId, argumentString);
} else if (command == "serverreload") {
return serverReload(connectionId, argumentString);
} else if (command == "eval") {
return eval(connectionId, argumentString);
} else if (command == "entityeval") {
return entityEval(connectionId, argumentString);
} else if (command == "enablespawning") {
return enableSpawning(connectionId, argumentString);
} else if (command == "disablespawning") {
return disableSpawning(connectionId, argumentString);
} else if (command == "placedungeon") {
return placeDungeon(connectionId, argumentString);
} else if (command == "setuniverseflag") {
return setUniverseFlag(connectionId, argumentString);
} else if (command == "resetuniverseflags") {
return resetUniverseFlags(connectionId, argumentString);
} else if (command == "addbiomeregion") {
return addBiomeRegion(connectionId, argumentString);
} else if (command == "expandbiomeregion") {
return expandBiomeRegion(connectionId, argumentString);
} else if (command == "updateplanettype") {
return updatePlanetType(connectionId, argumentString);
} else if (command == "setenvironmentbiome") {
return setEnvironmentBiome(connectionId, argumentString);
} else if (auto res = m_scriptComponent.invoke("command", command, connectionId, jsonFromStringList(m_parser.tokenizeToStringList(argumentString)))) {
return toString(*res);
} else {
2023-06-27 10:23:44 +00:00
return strf("No such command {}", command);
2023-06-20 04:33:09 +00:00
}
}
Maybe<String> CommandProcessor::adminCheck(ConnectionId connectionId, String const& commandDescription) const {
if (connectionId == ServerConnectionId)
return {};
auto config = Root::singleton().configuration();
if (!config->get("allowAdminCommands").toBool())
return {"Admin commands disabled on this server."};
if (!config->get("allowAdminCommandsFromAnyone").toBool()) {
if (!m_universe->isAdmin(connectionId))
2023-06-27 10:23:44 +00:00
return {strf("Insufficient privileges to {}.", commandDescription)};
2023-06-20 04:33:09 +00:00
}
return {};
}
Maybe<String> CommandProcessor::localCheck(ConnectionId connectionId, String const& commandDescription) const {
if (connectionId == ServerConnectionId)
return {};
if (!m_universe->isLocal(connectionId))
2023-06-27 10:23:44 +00:00
return {strf("The {} command can only be used locally.", commandDescription)};
2023-06-20 04:33:09 +00:00
return {};
}
LuaCallbacks CommandProcessor::makeCommandCallbacks() {
LuaCallbacks callbacks;
callbacks.registerCallbackWithSignature<Maybe<String>, ConnectionId, String>(
"adminCheck", bind(&CommandProcessor::adminCheck, this, _1, _2));
return callbacks;
}
}