diff options
Diffstat (limited to 'source/game/StarCommandProcessor.cpp')
-rw-r--r-- | source/game/StarCommandProcessor.cpp | 1021 |
1 files changed, 1021 insertions, 0 deletions
diff --git a/source/game/StarCommandProcessor.cpp b/source/game/StarCommandProcessor.cpp new file mode 100644 index 0000000..ae7a913 --- /dev/null +++ b/source/game/StarCommandProcessor.cpp @@ -0,0 +1,1021 @@ +#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"))); + m_scriptComponent.setLuaRoot(make_shared<LuaRoot>()); + 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) + return strf("Admin privileges now given to player %s", m_universe->clientNick(connectionId)); + else + return strf("Admin privileges taken away from %s", m_universe->clientNick(connectionId)); + } 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)) + m_universe->adminBroadcast(strf("Player %s is now PVP", m_universe->clientNick(connectionId))); + } else { + m_universe->setPvp(connectionId, false); + if (!m_universe->isPvp(connectionId)) + m_universe->adminBroadcast(strf("Player %s is a big wimp and is no longer PVP", m_universe->clientNick(connectionId))); + } + + if (m_universe->isPvp(connectionId)) + return "PVP active"; + else + return "PVP inactive"; +} + +String CommandProcessor::whoami(ConnectionId connectionId, String const&) { + return strf("Server: You are %s. You are %san Admin", + 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) { + Logger::warn("Could not parse warp target: %s", outputException(e, false)); + return strf("Could not parse the argument %s as a warp target", argumentString); + } +} + +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; + } + } + } + } + } + + if (size.magnitude() > 1024) + return "could not find a matching world"; + size *= 2; + } + + m_universe->clientWarpPlayer(connectionId, WarpToWorld(CelestialWorldId(*target))); + return strf("warping to %s", *target); +} + +String CommandProcessor::timewarp(ConnectionId connectionId, String const& argumentString) { + if (auto errorMsg = adminCheck(connectionId, "do the time warp again")) + return *errorMsg; + + try { + auto time = lexicalCast<double>(argumentString); + if (time < 0) + return "Great Scott! We can't go back in time!"; + + m_universe->universeClock()->adjustTime(time); + return "It's just a jump to the left..."; + } catch (BadLexicalCast const&) { + return strf("Could not parse the argument %s as a time adjustment", argumentString); + } +} + +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.size() == 0) + 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), player->aimPosition())); + }); + + return done ? "" : "Invalid client state"; + } catch (JsonParsingException const& exception) { + Logger::warn("Error while processing /spawnitem '%s' command. Json parse problem: %s", arguments.at(0), outputException(exception, false)); + return "Could not parse item parameters"; + } catch (ItemException const& exception) { + Logger::warn("Error while processing /spawnitem '%s' command. Item instantiation problem: %s", arguments.at(0), outputException(exception, false)); + return strf("Could not load item '%s'", arguments.at(0)); + } catch (BadLexicalCast const& exception) { + Logger::warn("Error while processing /spawnitem command. Number expected. Got something else: %s", outputException(exception, false)); + return strf("Could not load item '%s'", arguments.at(0)); + } catch (StarException const& exception) { + Logger::warn("Error while processing /spawnitem command '%s', exception caught: %s", argumentString, outputException(exception, false)); + return strf("Could not load item '%s'", arguments.at(0)); + } +} + +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.size() == 0) + 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) { + Logger::warn("Error while processing /spawntreasure '%s' command. Json parse problem: %s", arguments.at(0), outputException(exception, false)); + return "Could not parse item parameters"; + } catch (ItemException const& exception) { + Logger::warn("Error while processing /spawntreasure '%s' command. Item instantiation problem: %s", arguments.at(0), outputException(exception, false)); + return strf("Could not load item '%s'", arguments.at(0)); + } catch (BadLexicalCast const& exception) { + Logger::warn("Error while processing /spawntreasure command. Number expected. Got something else: %s", outputException(exception, false)); + return strf("Could not load item '%s'", arguments.at(0)); + } catch (StarException const& exception) { + Logger::warn("Error while processing /spawntreasure command '%s', exception caught: %s", argumentString, outputException(exception, false)); + return strf("Could not load item '%s'", arguments.at(0)); + } +} + +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) { + Logger::warn("Could not spawn Monster of type '%s', exception caught: %s", argumentString, outputException(exception, false)); + return strf("Could not spawn Monster of type '%s'", argumentString); + } +} + +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) { + Logger::warn("Could not spawn NPC of species '%s', exception caught: %s", argumentString, outputException(exception, true)); + return strf("Could not spawn NPC of species '%s'", argumentString); + } +} + +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(move(vehicle)); + }); + + return done ? "" : "Invalid client state"; + } catch (StarException const& exception) { + Logger::warn("Could not spawn vehicle, exception caught: %s", outputException(exception, false)); + 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) { + Logger::warn("Could not spawn Stagehand of type '%s', exception caught: %s", argumentString, outputException(exception, false)); + return strf("Could not spawn Stagehand of type '%s'", argumentString); + } +} + +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; + } + }); + return done ? strf("Removed %s stagehands", removed) : "Invalid client state"; +} + +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))) + return strf("No such liquid %d", arguments.at(0)); + + 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 + return strf("Could not parse quantity value '%s'", arguments.at(1)); + } + + 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( + "Could not spawn liquid '%s', exception caught: %s", argumentString, outputException(exception, false)); + 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.size() == 0) + return "No player specified"; + + auto toKick = playerCidFromCommand(arguments[0], m_universe); + if (!toKick) + return strf("No user with specifier %s found.", arguments[0]); + + // 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]); + + return strf("Successfully kicked user with specifier %s. ConnectionId: %s. Reason given: %s", + 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.size() == 0) + return "No player specified"; + + auto toKick = playerCidFromCommand(arguments[0], m_universe); + if (!toKick) + return strf("No user with specifier %s found.", arguments[0]); + + 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 { + return strf("Invalid argument %s passed as ban type to /ban. Options are ip, uuid, or both.", arguments[2]); + } + } + + Maybe<int> banTime; + if (arguments.size() == 4) { + try { + banTime = lexicalCast<int>(arguments[3]); + } catch (BadLexicalCast const&) { + return strf("Invalid argument %s passed as ban time to /ban.", arguments[3]); + } + } + + m_universe->banUser(*toKick, reason, type, banTime); + + return strf("Successfully kicked user with specifier %s. ConnectionId: %s. Reason given: %s", + 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.size() == 0) + return "No IP specified"; + + bool success = m_universe->unbanIp(arguments[0]); + + if (success) + return strf("Successfully removed IP %s from ban list", arguments[0]); + else + return strf("'%s' is not a valid IP or was not found in the bans list", arguments[0]); +} + +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.size() == 0) + return "No UUID specified"; + + bool success = m_universe->unbanUuid(arguments[0]); + + if (success) + return strf("Successfully removed UUID %s from ban list", arguments[0]); + else + return strf("'%s' is not a valid UUID or was not found in the bans list", arguments[0]); +} + +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()) + res.append(strf("$%s : %s : $$%s", cid, m_universe->clientNick(cid), m_universe->uuidForClient(cid)->hex())); + + 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) + return strf("No user with specifier %s found.", arguments[0]); + targetClientId = *cid; + targetLabel = strf("Client %s's", arguments[0]); + } + } + + if (targetClientId) { + auto worldId = m_universe->clientWorld(targetClientId); + return strf("%s current location is %s", targetLabel, worldId); + } 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); + }); + + return done ? strf("added region of biome %s with width %s", biomeName, width) : "failed to add biome region"; +} + +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); + }); + + return done ? strf("expanded region to width %s", newWidth) : "failed to expand biome region"; +} + +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); + + return done ? strf("set planet at %s to type %s weatherBiome %s", coordinate, newType, weatherBiome) : "failed to update planet type"; +} + +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); +} + +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 == "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 { + return strf("No such command %s", command); + } +} + +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)) + return {strf("Insufficient privileges to %s.", commandDescription)}; + } + + return {}; +} + +Maybe<String> CommandProcessor::localCheck(ConnectionId connectionId, String const& commandDescription) const { + if (connectionId == ServerConnectionId) + return {}; + + if (!m_universe->isLocal(connectionId)) + return {strf("The %s command can only be used locally.", commandDescription)}; + + return {}; +} + +LuaCallbacks CommandProcessor::makeCommandCallbacks() { + LuaCallbacks callbacks; + callbacks.registerCallbackWithSignature<Maybe<String>, ConnectionId, String>( + "adminCheck", bind(&CommandProcessor::adminCheck, this, _1, _2)); + return callbacks; +} + +} |